From 7e961fc1571002cfea45c9d0866a4e0f984f6809 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 17 Jan 2021 17:21:15 +0000 Subject: [PATCH 001/152] Make datapoint count more efficient. --- .../LineChart/Views/LineChartView.swift | 2 +- .../Shared/Extras/Calculations.swift | 18 +++++++++++------- .../Shared/Models/ChartData.swift | 8 ++++++++ .../Shared/ViewModifiers/HeaderBox.swift | 2 +- .../Shared/ViewModifiers/PointMarkers.swift | 2 +- .../Shared/ViewModifiers/TouchOverlay.swift | 2 +- .../Shared/ViewModifiers/XAxisGrid.swift | 2 +- .../Shared/ViewModifiers/YAxisGrid.swift | 2 +- .../Shared/ViewModifiers/YAxisLabels.swift | 4 ++-- .../Shared/ViewModifiers/YAxisPOI.swift | 2 +- 10 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index 8eb8b5a2..96147c67 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -24,7 +24,7 @@ internal struct LineChartView: View { let style : LineStyle = chartData.lineStyle let strokeStyle = style.strokeStyle - if chartData.dataPoints.count > 2 { + if chartData.isGreaterThanTwo { if style.colourType == .colour, let colour = style.colour diff --git a/Sources/SwiftUICharts/Shared/Extras/Calculations.swift b/Sources/SwiftUICharts/Shared/Extras/Calculations.swift index 363e33e1..99188b49 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Calculations.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Calculations.swift @@ -45,6 +45,7 @@ internal struct Calculations { pointLabel: formatterForPointLabel.string(from: date))) } } + return outputData } @@ -71,7 +72,7 @@ internal struct Calculations { var outputData : [ChartDataPoint] = [] for index in 0...numberOfWeeks { - if let date = calendar.date(byAdding: .weekOfYear, value: index, to: firstDataPoint) { + if let date = calendar.date(byAdding: .weekOfYear, value: (index), to: firstDataPoint) { let requestedWeek = calendar.dateComponents([.year, .weekOfYear], from: date) @@ -81,12 +82,13 @@ internal struct Calculations { } 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 } @@ -106,26 +108,28 @@ internal struct Calculations { 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 } + guard let numberOfDays = calendar.dateComponents([.day], + from: firstDataPoint, + to: lastDataPoint).day else { return nil } var outputData : [ChartDataPoint] = [] - for index in 0...numberOfWeeks { + for index in 0...numberOfDays { 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) - + if !average.isNaN { outputData.append(ChartDataPoint(value: average, xAxisLabel: formatterForXAxisLabel.string(from: date), pointLabel: formatterForPointLabel.string(from: date))) + } } } return outputData diff --git a/Sources/SwiftUICharts/Shared/Models/ChartData.swift b/Sources/SwiftUICharts/Shared/Models/ChartData.swift index f6ee4807..f50424bb 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartData.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartData.swift @@ -35,6 +35,8 @@ public class ChartData: ObservableObject, Identifiable { @Published var viewData : ChartViewData public var noDataText : Text = Text("No Data") + + var isGreaterThanTwo: Bool = true // MARK: - init: Calculations /// ChartData is the central model from which the chart is drawn. @@ -75,6 +77,8 @@ public class ChartData: ObservableObject, Identifiable { self.pointStyle = pointStyle self.legends = [LegendData]() self.viewData = ChartViewData() + + greaterThanTwo() } // MARK: - init: Custom Calculations @@ -107,6 +111,7 @@ public class ChartData: ObservableObject, Identifiable { self.legends = [LegendData]() self.viewData = ChartViewData() + greaterThanTwo() } // MARK: - Functions @@ -138,6 +143,9 @@ public class ChartData: ObservableObject, Identifiable { */ return (maxValue - minValue) + 0.001 } + func greaterThanTwo() { + self.isGreaterThanTwo = dataPoints.count > 2 + } /// Sets the order the Legends are layed out in. /// - Returns: Ordered array of Legends. diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index 1f51b107..893c6116 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -61,7 +61,7 @@ internal struct HeaderBox: ViewModifier { @ViewBuilder internal func body(content: Content) -> some View { - if chartData.dataPoints.count > 2 { + if chartData.isGreaterThanTwo { #if !os(tvOS) if chartData.chartStyle.infoBoxPlacement == .floating { VStack(alignment: .leading) { diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift index 70dd6bed..8577230c 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift @@ -16,7 +16,7 @@ internal struct PointMarkers: ViewModifier { let pointStyle = chartData.pointStyle return ZStack { content - if chartData.dataPoints.count > 2 { + if chartData.isGreaterThanTwo { switch pointStyle.pointType { case .filled: Point(chartData: chartData, pointSize: pointStyle.pointSize, pointType: pointStyle.pointShape, chartType: chartData.viewData.chartType) diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 57078810..8bc9f95b 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -41,7 +41,7 @@ internal struct TouchOverlay: ViewModifier { } @ViewBuilder internal func body(content: Content) -> some View { - if chartData.dataPoints.count > 2 { + if chartData.isGreaterThanTwo { GeometryReader { geo in ZStack { content diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift index 7522151c..b24bb019 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift @@ -13,7 +13,7 @@ internal struct XAxisGrid: ViewModifier { internal func body(content: Content) -> some View { ZStack { - if chartData.dataPoints.count > 2 { + if chartData.isGreaterThanTwo { HStack { ForEach((0...chartData.chartStyle.xAxisGridStyle.numberOfLines), id: \.self) { index in if index != 0 { diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift index cece4568..92f403a6 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift @@ -13,7 +13,7 @@ internal struct YAxisGrid: ViewModifier { internal func body(content: Content) -> some View { ZStack { - if chartData.dataPoints.count > 2 { + if chartData.isGreaterThanTwo { VStack { ForEach((0...chartData.chartStyle.yAxisGridStyle.numberOfLines), id: \.self) { index in if index != 0 { diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift index 9788c32a..ced46606 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift @@ -61,7 +61,7 @@ internal struct YAxisLabels: ViewModifier { switch chartData.chartStyle.yAxisLabelPosition { case .leading: HStack { - if chartData.dataPoints.count > 2 { + if chartData.isGreaterThanTwo { labels } content @@ -69,7 +69,7 @@ internal struct YAxisLabels: ViewModifier { case .trailing: HStack { content - if chartData.dataPoints.count > 2 { + if chartData.isGreaterThanTwo { labels } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift index e1b46aa7..bfe508d6 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift @@ -38,7 +38,7 @@ internal struct YAxisPOI: ViewModifier { internal func body(content: Content) -> some View { ZStack { content - if chartData.dataPoints.count > 2 { + if chartData.isGreaterThanTwo { Marker(chartData: chartData, markerValue: markerValue, isAverage: isAverage, chartType: chartData.viewData.chartType) .stroke(lineColour, style: strokeStyle) .onAppear { From 4caefbd7fce20e2cffb6b05bf6c417852ee2743f Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 17 Jan 2021 17:21:27 +0000 Subject: [PATCH 002/152] Update tests. --- .../SwiftUIChartsTests.swift | 252 +++++++++++++++--- 1 file changed, 213 insertions(+), 39 deletions(-) diff --git a/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift b/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift index 55d9318b..7444054a 100644 --- a/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift +++ b/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift @@ -11,7 +11,7 @@ final class SwiftUIChartsTests: XCTestCase { ChartDataPoint(value: 30), ChartDataPoint(value: 60) ] - let chartData = ChartData(dataPoints: dataPoints, lineChartStyle: ChartStyle()) + let chartData = ChartData(dataPoints: dataPoints) XCTAssertEqual(chartData.maxValue(), 60) } @@ -22,7 +22,7 @@ final class SwiftUIChartsTests: XCTestCase { ChartDataPoint(value: 30), ChartDataPoint(value: 60) ] - let chartData = ChartData(dataPoints: dataPoints, lineChartStyle: ChartStyle()) + let chartData = ChartData(dataPoints: dataPoints) XCTAssertEqual(chartData.minValue(), 10) } @@ -33,7 +33,7 @@ final class SwiftUIChartsTests: XCTestCase { ChartDataPoint(value: 30), ChartDataPoint(value: 60) ] - let chartData = ChartData(dataPoints: dataPoints, lineChartStyle: ChartStyle()) + let chartData = ChartData(dataPoints: dataPoints) XCTAssertEqual(chartData.average(), 35) } @@ -44,29 +44,44 @@ final class SwiftUIChartsTests: XCTestCase { ChartDataPoint(value: 30), ChartDataPoint(value: 60) ] - let chartData = ChartData(dataPoints: dataPoints, lineChartStyle: ChartStyle()) + let chartData = ChartData(dataPoints: dataPoints) XCTAssertEqual(chartData.range(), 50.001) } - // MARK: - Helper + + // MARK: - Calculations func testMonthlyAverage() { let calendar = Calendar.current let formatterForXAxisLabel = DateFormatter() formatterForXAxisLabel.locale = .current formatterForXAxisLabel.setLocalizedDateFormatFromTemplate("MMM") - let formatterForPointLabel = DateFormatter() - formatterForXAxisLabel.locale = .current + formatterForPointLabel.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)! + guard let date = calendar.date(from: components) else { + XCTFail("date failed") + return + } + guard let monthOne = calendar.date(byAdding: .month, value: 0, to: date) else { + XCTFail("monthOne failed") + return + } + guard let monthTwo = calendar.date(byAdding: .month, value: 1, to: date) else { + XCTFail("monthTwo failed") + return + } + guard let monthThree = calendar.date(byAdding: .month, value: 2, to: date) else { + XCTFail("monthThree failed") + return + } + guard let monthFour = calendar.date(byAdding: .month, value: 3, to: date) else { + XCTFail("monthFour failed") + return + } let dataPoints = [ ChartDataPoint(value: 10, date: calendar.date(byAdding: .day, value: 0, to: monthOne)), @@ -75,38 +90,194 @@ final class SwiftUIChartsTests: XCTestCase { 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: 50, date: calendar.date(byAdding: .day, value: 6, to: monthTwo)), + ChartDataPoint(value: 70, date: calendar.date(byAdding: .day, value: 19, to: monthTwo)), + ChartDataPoint(value: 30, date: calendar.date(byAdding: .day, value: 27, 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: 50, date: calendar.date(byAdding: .day, value: 3, to: monthThree)), + ChartDataPoint(value: 40, date: calendar.date(byAdding: .day, value: 10, to: monthThree)), + ChartDataPoint(value: 80, date: calendar.date(byAdding: .day, value: 20, 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)) + ChartDataPoint(value: 40, date: calendar.date(byAdding: .day, value: 2, to: monthFour)), + ChartDataPoint(value: 20, date: calendar.date(byAdding: .day, value: 25, to: monthFour)), + ChartDataPoint(value: 10, date: calendar.date(byAdding: .day, value: 26, 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)), - ]) + guard let monthlyAverage = Calculations.monthlyAverage(dataPoints: dataPoints) else { + XCTFail("Failed") + return + } + + + XCTAssertEqual(monthlyAverage[0].value, 35.0) + XCTAssertEqual(monthlyAverage[1].value, 52.5) + XCTAssertEqual(monthlyAverage[2].value, 47.5) + XCTAssertEqual(monthlyAverage[3].value, 35.0) + + XCTAssertEqual(monthlyAverage[0].xAxisLabel, formatterForXAxisLabel.string(from: monthOne)) + XCTAssertEqual(monthlyAverage[1].xAxisLabel, formatterForXAxisLabel.string(from: monthTwo)) + XCTAssertEqual(monthlyAverage[2].xAxisLabel, formatterForXAxisLabel.string(from: monthThree)) + XCTAssertEqual(monthlyAverage[3].xAxisLabel, formatterForXAxisLabel.string(from: monthFour)) + + XCTAssertEqual(monthlyAverage[0].pointDescription, formatterForPointLabel.string(from: monthOne)) + XCTAssertEqual(monthlyAverage[1].pointDescription, formatterForPointLabel.string(from: monthTwo)) + XCTAssertEqual(monthlyAverage[2].pointDescription, formatterForPointLabel.string(from: monthThree)) + XCTAssertEqual(monthlyAverage[3].pointDescription, formatterForPointLabel.string(from: monthFour)) } + func testWeeklyAverage() { + let calendar = Calendar.current + + let formatterForXAxisLabel = DateFormatter() + formatterForXAxisLabel.locale = .current + formatterForXAxisLabel.setLocalizedDateFormatFromTemplate("d") + let formatterForPointLabel = DateFormatter() + formatterForPointLabel.locale = .current + formatterForPointLabel.setLocalizedDateFormatFromTemplate("MMMM YYYY") + + let components = DateComponents(year: 2021, month: 01, day: 03, hour: 10, minute: 0, second: 0) + + guard let date = calendar.date(from: components) else { + XCTFail("date failed") + return + } + guard let weekOne = calendar.date(byAdding: .day, value: 1, to: date) else { + XCTFail("monthOne failed") + return + } + guard let weekTwo = calendar.date(byAdding: .day, value: 8, to: date) else { + XCTFail("monthTwo failed") + return + } + guard let weekThree = calendar.date(byAdding: .day, value: 15, to: date) else { + XCTFail("monthThree failed") + return + } + guard let weekFour = calendar.date(byAdding: .day, value: 22, to: date) else { + XCTFail("monthFour failed") + return + } + + let dataPoints = [ + ChartDataPoint(value: 30, date: calendar.date(byAdding: .day, value: 0, to: weekOne)), + ChartDataPoint(value: 40, date: calendar.date(byAdding: .day, value: 1, to: weekOne)), + ChartDataPoint(value: 60, date: calendar.date(byAdding: .day, value: 3, to: weekOne)), + ChartDataPoint(value: 80, date: calendar.date(byAdding: .day, value: 5, to: weekOne)), + + ChartDataPoint(value: 40, date: calendar.date(byAdding: .day, value: 0, to: weekTwo)), + ChartDataPoint(value: 20, date: calendar.date(byAdding: .day, value: 2, to: weekTwo)), + ChartDataPoint(value: 70, date: calendar.date(byAdding: .day, value: 3, to: weekTwo)), + ChartDataPoint(value: 90, date: calendar.date(byAdding: .day, value: 5, to: weekTwo)), + + ChartDataPoint(value: 10, date: calendar.date(byAdding: .day, value: 1, to: weekThree)), + ChartDataPoint(value: 50, date: calendar.date(byAdding: .day, value: 2, to: weekThree)), + ChartDataPoint(value: 30, date: calendar.date(byAdding: .day, value: 4, to: weekThree)), + ChartDataPoint(value: 80, date: calendar.date(byAdding: .day, value: 5, to: weekThree)), + + ChartDataPoint(value: 60, date: calendar.date(byAdding: .day, value: 0, to: weekFour)), + ChartDataPoint(value: 40, date: calendar.date(byAdding: .day, value: 2, to: weekFour)), + ChartDataPoint(value: 80, date: calendar.date(byAdding: .day, value: 3, to: weekFour)), + ChartDataPoint(value: 40, date: calendar.date(byAdding: .day, value: 5, to: weekFour)) + ] + + guard let weeklyAverage = Calculations.weeklyAverage(dataPoints: dataPoints) else { + XCTFail("Failed") + return + } + + XCTAssertEqual(weeklyAverage[0].value, 52.5) + XCTAssertEqual(weeklyAverage[1].value, 55.0) + XCTAssertEqual(weeklyAverage[2].value, 42.5) + XCTAssertEqual(weeklyAverage[3].value, 55.0) + + XCTAssertEqual(weeklyAverage[0].xAxisLabel, formatterForXAxisLabel.string(from: weekOne)) + XCTAssertEqual(weeklyAverage[1].xAxisLabel, formatterForXAxisLabel.string(from: weekTwo)) + XCTAssertEqual(weeklyAverage[2].xAxisLabel, formatterForXAxisLabel.string(from: weekThree)) + XCTAssertEqual(weeklyAverage[3].xAxisLabel, formatterForXAxisLabel.string(from: weekFour)) + + XCTAssertEqual(weeklyAverage[0].pointDescription, formatterForPointLabel.string(from: weekOne)) + XCTAssertEqual(weeklyAverage[1].pointDescription, formatterForPointLabel.string(from: weekTwo)) + XCTAssertEqual(weeklyAverage[2].pointDescription, formatterForPointLabel.string(from: weekThree)) + XCTAssertEqual(weeklyAverage[3].pointDescription, formatterForPointLabel.string(from: weekFour)) + } + func testDailyAverage() { + let calendar = Calendar.current + + let formatterForXAxisLabel = DateFormatter() + formatterForXAxisLabel.locale = .current + formatterForXAxisLabel.setLocalizedDateFormatFromTemplate("d") + let formatterForPointLabel = DateFormatter() + formatterForPointLabel.locale = .current + formatterForPointLabel.setLocalizedDateFormatFromTemplate("dd MMMM YYYY") + + let components = DateComponents(year: 2021, month: 01, day: 03, hour: 10, minute: 0, second: 0) + + guard let date = calendar.date(from: components) else { + XCTFail("date failed") + return + } + guard let dayOne = calendar.date(byAdding: .day, value: 1, to: date) else { + XCTFail("monthOne failed") + return + } + guard let dayTwo = calendar.date(byAdding: .day, value: 2, to: date) else { + XCTFail("monthTwo failed") + return + } + guard let dayThree = calendar.date(byAdding: .day, value: 3, to: date) else { + XCTFail("monthThree failed") + return + } + guard let dayFour = calendar.date(byAdding: .day, value: 4, to: date) else { + XCTFail("monthFour failed") + return + } + + let dataPoints = [ + ChartDataPoint(value: 30, date: calendar.date(byAdding: .hour, value: 0, to: dayOne)), + ChartDataPoint(value: 40, date: calendar.date(byAdding: .hour, value: 1, to: dayOne)), + ChartDataPoint(value: 60, date: calendar.date(byAdding: .hour, value: 3, to: dayOne)), + ChartDataPoint(value: 80, date: calendar.date(byAdding: .hour, value: 5, to: dayOne)), + + ChartDataPoint(value: 40, date: calendar.date(byAdding: .hour, value: 0, to: dayTwo)), + ChartDataPoint(value: 20, date: calendar.date(byAdding: .hour, value: 2, to: dayTwo)), + ChartDataPoint(value: 70, date: calendar.date(byAdding: .hour, value: 3, to: dayTwo)), + ChartDataPoint(value: 90, date: calendar.date(byAdding: .hour, value: 5, to: dayTwo)), + + ChartDataPoint(value: 10, date: calendar.date(byAdding: .hour, value: 1, to: dayThree)), + ChartDataPoint(value: 50, date: calendar.date(byAdding: .hour, value: 2, to: dayThree)), + ChartDataPoint(value: 30, date: calendar.date(byAdding: .hour, value: 4, to: dayThree)), + ChartDataPoint(value: 80, date: calendar.date(byAdding: .hour, value: 5, to: dayThree)), + + ChartDataPoint(value: 60, date: calendar.date(byAdding: .hour, value: 0, to: dayFour)), + ChartDataPoint(value: 40, date: calendar.date(byAdding: .hour, value: 2, to: dayFour)), + ChartDataPoint(value: 80, date: calendar.date(byAdding: .hour, value: 3, to: dayFour)), + ChartDataPoint(value: 40, date: calendar.date(byAdding: .hour, value: 5, to: dayFour)) + ] + + guard let dailyAverage = Calculations.dailyAverage(dataPoints: dataPoints) else { + XCTFail("Failed") + return + } + + XCTAssertEqual(dailyAverage[0].value, 52.5) + XCTAssertEqual(dailyAverage[1].value, 55.0) + XCTAssertEqual(dailyAverage[2].value, 42.5) + XCTAssertEqual(dailyAverage[3].value, 55.0) + + XCTAssertEqual(dailyAverage[0].xAxisLabel, formatterForXAxisLabel.string(from: dayOne)) + XCTAssertEqual(dailyAverage[1].xAxisLabel, formatterForXAxisLabel.string(from: dayTwo)) + XCTAssertEqual(dailyAverage[2].xAxisLabel, formatterForXAxisLabel.string(from: dayThree)) + XCTAssertEqual(dailyAverage[3].xAxisLabel, formatterForXAxisLabel.string(from: dayFour)) + + XCTAssertEqual(dailyAverage[0].pointDescription, formatterForPointLabel.string(from: dayOne)) + XCTAssertEqual(dailyAverage[1].pointDescription, formatterForPointLabel.string(from: dayTwo)) + XCTAssertEqual(dailyAverage[2].pointDescription, formatterForPointLabel.string(from: dayThree)) + XCTAssertEqual(dailyAverage[3].pointDescription, formatterForPointLabel.string(from: dayFour)) + } static var allTests = [ // Chart Data @@ -114,7 +285,10 @@ final class SwiftUIChartsTests: XCTestCase { ("testMinValue", testMinValue), ("testAverage", testAverage), ("testRange", testRange), - ("testMonthlyAverage", testMonthlyAverage) - // Helper + + // Calculations + ("testMonthlyAverage", testMonthlyAverage), + ("testWeeklyAverage", testWeeklyAverage), + ("testDailyAverage", testDailyAverage) ] } From d377d9a8616459c9af7b3db3474b5358dd682bfc Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 22 Jan 2021 07:23:41 +0000 Subject: [PATCH 003/152] Data model test --- .../BarChart/Views/BarChartView.swift | 2 +- .../LineChart/Shapes/LineShape.swift | 134 +++++++++++------- .../LineChart/Views/LineChart.swift | 22 --- .../LineChart/Views/LineChartView.swift | 101 ++++++++----- .../Shared/Models/ChartData.swift | 63 +++++--- .../SwiftUICharts/Shared/Shapes/Marker.swift | 15 +- .../Shared/Shapes/PointShape.swift | 56 ++++---- .../Shared/ViewModifiers/PointMarkers.swift | 8 +- .../Shared/ViewModifiers/TouchOverlay.swift | 30 ++-- .../Shared/ViewModifiers/YAxisLabels.swift | 8 +- .../Shared/ViewModifiers/YAxisPOI.swift | 2 +- 11 files changed, 259 insertions(+), 182 deletions(-) delete mode 100644 Sources/SwiftUICharts/LineChart/Views/LineChart.swift diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift b/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift index 7fabb15f..924991a4 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift @@ -20,7 +20,7 @@ internal struct BarChartView: View { internal var body: some View { - let maxValue: Double = chartData.maxValue() + let maxValue: Double = DataFunctions.maxValue(dataPoints: chartData.dataPoints) let style : BarStyle = chartData.barStyle return HStack(spacing: 0) { diff --git a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift index 15545510..32b50b10 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift @@ -9,83 +9,109 @@ import SwiftUI internal struct LineShape: Shape { - private let chartData : ChartData + private let dataPoints : [ChartDataPoint] /// Drawing style of the line private let lineType : LineType /// If it's to be filled some extra lines need to be drawn private let isFilled : Bool - internal init(chartData : ChartData, + internal init(dataPoints : [ChartDataPoint], lineType : LineType, isFilled : Bool ) { - self.chartData = chartData + self.dataPoints = dataPoints self.lineType = lineType self.isFilled = isFilled } internal func path(in rect: CGRect) -> Path { - let minValue: Double = chartData.minValue() - let range : Double = chartData.range() - - var path = Path() - - let x : CGFloat = rect.width / CGFloat(chartData.dataPoints.count - 1) - let y : CGFloat = rect.height / CGFloat(range) - - let firstPoint = CGPoint(x: 0, - y: (CGFloat(chartData.dataPoints[0].value - minValue) * -y) + rect.height) - path.move(to: firstPoint) - - var previousPoint = firstPoint - - if !chartData.lineStyle.ignoreZero { - for index in 1 ..< chartData.dataPoints.count - 1 { - let nextPoint = CGPoint(x: CGFloat(index) * x, - y: (CGFloat(chartData.dataPoints[index].value - minValue) * -y) + rect.height) - lineSwitch(&path, nextPoint, previousPoint) - previousPoint = nextPoint - } - } else { - for index in 1 ..< chartData.dataPoints.count - 1 { - if chartData.dataPoints[index].value != 0 { - let nextPoint = CGPoint(x: CGFloat(index) * x, - y: (CGFloat(chartData.dataPoints[index].value - minValue) * -y) + rect.height) - lineSwitch(&path, nextPoint, previousPoint) - previousPoint = nextPoint - } - } - } - - let lastPoint = CGPoint(x: CGFloat(chartData.dataPoints.count-1) * x, - y: (CGFloat(chartData.dataPoints[chartData.dataPoints.count-1].value - minValue) * -y) + rect.height) - lineSwitch(&path, lastPoint, previousPoint) - - if isFilled { - // Draw line straight down - path.addLine(to: CGPoint(x: CGFloat(chartData.dataPoints.count-1) * 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 - } - - internal func lineSwitch(_ path: inout Path, _ nextPoint: CGPoint, _ previousPoint: CGPoint) { + let minValue: Double = DataFunctions.minValue(dataPoints: dataPoints) + let range : Double = DataFunctions.range(dataPoints: dataPoints) + + let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1) + let y : CGFloat = rect.height / CGFloat(range) + switch lineType { + case .curvedLine: + return curvedLine(rect, x, y, dataPoints, minValue, range, isFilled) case .line: + return straightLine(rect, x, y, dataPoints, minValue, range, isFilled) + } + + } +} + +extension LineShape { + func straightLine(_ rect : CGRect, + _ x : CGFloat, + _ y : CGFloat, + _ dataPoints : [ChartDataPoint], + _ minValue : Double, + _ range : Double, + _ isFilled : Bool + ) -> Path { + + 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) path.addLine(to: nextPoint) - case .curvedLine: + } + + if isFilled { filled(&path, rect, x, y, dataPoints) } + + return path + } + + func curvedLine(_ rect : CGRect, + _ x : CGFloat, + _ y : CGFloat, + _ dataPoints : [ChartDataPoint], + _ minValue : Double, + _ range : Double, + _ isFilled : Bool + ) -> Path { + + var path = Path() + + let firstPoint = CGPoint(x: 0, + y: (CGFloat(dataPoints[0].value - minValue) * -y) + rect.height) + path.move(to: firstPoint) + + var previousPoint = firstPoint + + for index in 1 ..< dataPoints.count { + let nextPoint = CGPoint(x: CGFloat(index) * x, + y: (CGFloat(dataPoints[index].value - minValue) * -y) + rect.height) + 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 } + + if isFilled { filled(&path, rect, x, y, dataPoints) } + + return path + } + + func filled(_ path: inout Path, _ rect: CGRect, _ x : CGFloat, _ y : CGFloat, _ dataPoints: [ChartDataPoint]) { + // Draw line straight down + path.addLine(to: CGPoint(x: CGFloat(dataPoints.count-1) * 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() } } 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 96147c67..d575d22d 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -7,20 +7,15 @@ import SwiftUI -internal struct LineChartView: View { +public struct LineChart: View { @EnvironmentObject var chartData: ChartData @State var startAnimation : Bool = false + + public init() {} - let isFilled : Bool - - internal init(isFilled : Bool) { - self.isFilled = isFilled - } - - internal var body: some View { - + public var body: some View { let style : LineStyle = chartData.lineStyle let strokeStyle = style.strokeStyle @@ -29,31 +24,22 @@ internal struct LineChartView: View { if style.colourType == .colour, let colour = style.colour { - if !isFilled { - LineShape(chartData: chartData, lineType: style.lineType, isFilled: isFilled) + LineShape(dataPoints: chartData.dataPoints, lineType: style.lineType, isFilled: false) .trim(to: startAnimation ? 1 : 0) .stroke(colour, style: strokeStyle) .modifier(LineShapeModifiers(chartData)) .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true } - } else { - LineShape(chartData: chartData, lineType: style.lineType, isFilled: isFilled) - .trim(to: startAnimation ? 1 : 0) - .fill(colour) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - } + } else if style.colourType == .gradientColour, let colours = style.colours, let startPoint = style.startPoint, let endPoint = style.endPoint { - if !isFilled { - LineShape(chartData: chartData, lineType: style.lineType, isFilled: isFilled) + + LineShape(dataPoints: chartData.dataPoints, lineType: style.lineType, isFilled: false) .trim(to: startAnimation ? 1 : 0) .stroke(LinearGradient(gradient: Gradient(colors: colours), startPoint: startPoint, @@ -63,23 +49,15 @@ internal struct LineChartView: View { .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true } - } else { - LineShape(chartData: chartData, lineType: style.lineType, isFilled: isFilled) - .trim(to: startAnimation ? 1 : 0) - .fill(LinearGradient(gradient: Gradient(colors: colours), startPoint: startPoint, endPoint: endPoint)) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - } + } else if style.colourType == .gradientStops, let stops = style.stops, let startPoint = style.startPoint, let endPoint = style.endPoint { let stops = GradientStop.convertToGradientStopsArray(stops: stops) - if !isFilled { - LineShape(chartData: chartData, lineType: style.lineType, isFilled: isFilled) + + LineShape(dataPoints: chartData.dataPoints, lineType: style.lineType, isFilled: false) .trim(to: startAnimation ? 1 : 0) .stroke(LinearGradient(gradient: Gradient(stops: stops), startPoint: startPoint, @@ -89,8 +67,59 @@ internal struct LineChartView: View { .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true } - } else { - LineShape(chartData: chartData, lineType: style.lineType, isFilled: isFilled) + } + } else { CustomNoDataView(chartData: chartData) } + } +} + +public struct FilledLineChart: View { + + @EnvironmentObject var chartData: ChartData + + @State var startAnimation : Bool = false + + public init() {} + + public var body: some View { + + let style : LineStyle = chartData.lineStyle + + if chartData.isGreaterThanTwo { + + if style.colourType == .colour, + let colour = style.colour + { + + LineShape(dataPoints: chartData.dataPoints, lineType: style.lineType, isFilled: true) + .trim(to: startAnimation ? 1 : 0) + .fill(colour) + .modifier(LineShapeModifiers(chartData)) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + + } else if style.colourType == .gradientColour, + let colours = style.colours, + let startPoint = style.startPoint, + let endPoint = style.endPoint + { + + LineShape(dataPoints: chartData.dataPoints, lineType: style.lineType, isFilled: true) + .trim(to: startAnimation ? 1 : 0) + .fill(LinearGradient(gradient: Gradient(colors: colours), startPoint: startPoint, endPoint: endPoint)) + .modifier(LineShapeModifiers(chartData)) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + + } else if style.colourType == .gradientStops, + let stops = style.stops, + let startPoint = style.startPoint, + let endPoint = style.endPoint + { + let stops = GradientStop.convertToGradientStopsArray(stops: stops) + + LineShape(dataPoints: chartData.dataPoints, lineType: style.lineType, isFilled: true) .trim(to: startAnimation ? 1 : 0) .fill(LinearGradient(gradient: Gradient(stops: stops), startPoint: startPoint, @@ -99,7 +128,7 @@ internal struct LineChartView: View { .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true } - } + } } else { CustomNoDataView(chartData: chartData) } } diff --git a/Sources/SwiftUICharts/Shared/Models/ChartData.swift b/Sources/SwiftUICharts/Shared/Models/ChartData.swift index f50424bb..0da8537a 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartData.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartData.swift @@ -7,13 +7,38 @@ import SwiftUI +public protocol ChartData: ObservableObject, Identifiable { + var dataPoints : [ChartDataPoint] { get set } + + var metadata : ChartMetadata? { get set } + + var xAxisLabels : [String]? { get set } + + var chartStyle : ChartStyle { get set } + + var lineStyle : LineStyle + + var barStyle : BarStyle + var pointStyle : PointStyle + + var legends : [LegendData] + var viewData : ChartViewData + + public var noDataText : Text = Text("No Data") +} + +public protocol Style { + +} + /// The central model from which the chart is drawn. -public class ChartData: ObservableObject, Identifiable { +public class LineChartData: ChartData { 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? @@ -22,9 +47,10 @@ public class ChartData: ObservableObject, Identifiable { /// 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 @@ -114,26 +140,39 @@ public class ChartData: ObservableObject, Identifiable { greaterThanTwo() } + func greaterThanTwo() { + self.isGreaterThanTwo = dataPoints.count > 2 + } + + /// Sets the order the Legends are layed out in. + /// - Returns: Ordered array of Legends. + func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } +} + + +struct DataFunctions { // MARK: - Functions /// Get the highest value from dataPoints array. /// - Returns: Highest value. - func maxValue() -> Double { + static func maxValue(dataPoints: [ChartDataPoint]) -> Double { return dataPoints.max { $0.value < $1.value }?.value ?? 0 } /// Get the Lowest value from dataPoints array. /// - Returns: Lowest value. - func minValue() -> Double { + static func minValue(dataPoints: [ChartDataPoint]) -> Double { return dataPoints.min { $0.value < $1.value }?.value ?? 0 } /// Get the average of all the dataPoints. /// - Returns: Average. - func average() -> Double { + static func average(dataPoints: [ChartDataPoint]) -> 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 { + static func range(dataPoints: [ChartDataPoint]) -> Double { let maxValue = dataPoints.max { $0.value < $1.value }?.value ?? 0 let minValue = dataPoints.min { $0.value < $1.value }?.value ?? 0 @@ -143,16 +182,4 @@ public class ChartData: ObservableObject, Identifiable { */ return (maxValue - minValue) + 0.001 } - func greaterThanTwo() { - self.isGreaterThanTwo = dataPoints.count > 2 - } - - /// 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/Shapes/Marker.swift b/Sources/SwiftUICharts/Shared/Shapes/Marker.swift index 9c7f6c05..6fc2e486 100644 --- a/Sources/SwiftUICharts/Shared/Shapes/Marker.swift +++ b/Sources/SwiftUICharts/Shared/Shapes/Marker.swift @@ -10,18 +10,18 @@ import SwiftUI /// Generic line drawn horrizontally across the chart internal struct Marker: Shape { - private let chartData : ChartData + private let dataPoints : [ChartDataPoint] private let markerValue : Double private let isAverage : Bool private let chartType : ChartType - internal init(chartData : ChartData, + internal init(dataPoints : [ChartDataPoint], markerValue : Double = 0, isAverage : Bool, chartType : ChartType ) { - self.chartData = chartData + self.dataPoints = dataPoints self.markerValue = markerValue self.isAverage = isAverage self.chartType = chartType @@ -29,9 +29,10 @@ internal struct Marker: Shape { internal func path(in rect: CGRect) -> Path { - let range : Double = chartData.range() - let minValue: Double = chartData.minValue() - let value : Double = isAverage ? chartData.average() : markerValue + let range : Double = DataFunctions.range(dataPoints: dataPoints) + let minValue: Double = DataFunctions.minValue(dataPoints: dataPoints) + let maxValue: Double = DataFunctions.maxValue(dataPoints: dataPoints) + let value : Double = isAverage ? DataFunctions.average(dataPoints: dataPoints) : markerValue var path = Path() @@ -41,7 +42,7 @@ 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()) + let y = rect.height / CGFloat(maxValue) pointY = rect.height - CGFloat(value) * y } diff --git a/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift b/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift index e1c3f9d2..0b3e337c 100644 --- a/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift +++ b/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift @@ -9,20 +9,20 @@ import SwiftUI internal struct Point: Shape { - private let chartData : ChartData + private let dataPoints : [ChartDataPoint] private let pointSize : CGFloat private let pointType : PointShape private let cornerSize : Int private let chartType : ChartType - internal init(chartData : ChartData, + internal init(dataPoints: [ChartDataPoint], pointSize : CGFloat = 2, pointType : PointShape, cornerSize: Int = 3, chartType : ChartType ) { - self.chartData = chartData + self.dataPoints = dataPoints self.pointSize = pointSize self.pointType = pointType self.cornerSize = cornerSize @@ -34,22 +34,22 @@ internal struct Point: Shape { switch chartType { case .line: - lineChartDrawPoints(&path, rect, chartData.minValue(), chartData.range()) + lineChartDrawPoints(&path, rect, DataFunctions.minValue(dataPoints: dataPoints), DataFunctions.range(dataPoints: dataPoints)) case .bar: - barChartDrawPoints(&path, rect, chartData.minValue(), chartData.maxValue()) + barChartDrawPoints(&path, rect, DataFunctions.minValue(dataPoints: dataPoints), DataFunctions.maxValue(dataPoints: dataPoints)) } return path } internal func barChartDrawPoints(_ path: inout Path, _ rect: CGRect, _ minValue: Double, _ maxValue: Double) { - let x = rect.width / CGFloat(chartData.dataPoints.count) + let x = rect.width / CGFloat(dataPoints.count) let y = rect.height / CGFloat(maxValue) - for index in 0 ..< chartData.dataPoints.count { + for index in 0 ..< 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 pointY : CGFloat = (rect.height - (pointSize / CGFloat(2)) - CGFloat(dataPoints[index].value) * y) let point : CGRect = CGRect(x : pointX, y : pointY, @@ -61,43 +61,43 @@ internal struct Point: Shape { internal func lineChartDrawPoints(_ path: inout Path, _ rect: CGRect, _ minValue: Double, _ range: Double) { - let x = rect.width / CGFloat(chartData.dataPoints.count-1) + let x = rect.width / CGFloat(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 firstPointY : CGFloat = ((CGFloat(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 { +// if !chartData.lineStyle.ignoreZero { + for index in 1 ..< 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 pointY : CGFloat = ((CGFloat(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) - } - } - } +// } 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 lastPointX : CGFloat = (CGFloat(dataPoints.count-1) * x) - pointSize / CGFloat(2) + let lastPointY : CGFloat = ((CGFloat(dataPoints[dataPoints.count-1].value - minValue) * -y) + rect.height) - pointSize / CGFloat(2) let lastPoint : CGRect = CGRect(x : lastPointX, y : lastPointY, width : pointSize, diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift index 8577230c..b2a5d814 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift @@ -19,15 +19,15 @@ internal struct PointMarkers: ViewModifier { if chartData.isGreaterThanTwo { switch pointStyle.pointType { case .filled: - Point(chartData: chartData, pointSize: pointStyle.pointSize, pointType: pointStyle.pointShape, chartType: chartData.viewData.chartType) + Point(dataPoints: chartData.dataPoints, 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) + Point(dataPoints: chartData.dataPoints, 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) + Point(dataPoints: chartData.dataPoints, pointSize: pointStyle.pointSize, pointType: pointStyle.pointShape, chartType: chartData.viewData.chartType) .stroke(pointStyle.borderColour, lineWidth: pointStyle.lineWidth) - .background(Point(chartData: chartData, + .background(Point(dataPoints: chartData.dataPoints, pointSize: pointStyle.pointSize, pointType: pointStyle.pointShape, chartType: chartData.viewData.chartType) .foregroundColor(pointStyle.fillColour) diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 8bc9f95b..09f22027 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -40,8 +40,8 @@ internal struct TouchOverlay: ViewModifier { self.specifier = specifier } - @ViewBuilder internal func body(content: Content) -> some View { - if chartData.isGreaterThanTwo { + internal func body(content: Content) -> some View { +// if chartData.isGreaterThanTwo { GeometryReader { geo in ZStack { content @@ -85,7 +85,7 @@ internal struct TouchOverlay: ViewModifier { } } } - } else { content } +// } else { content } } // MARK: - Bar Chart @@ -107,8 +107,8 @@ internal struct TouchOverlay: ViewModifier { /// - chartSize: The size of the chart view as the parent view. internal func getPointLocationLineChart(touchLocation: CGPoint, chartSize: GeometryProxy) /* -> CGPoint */ { - let range = chartData.range() - let minValue = chartData.minValue() + let range = DataFunctions.range(dataPoints: chartData.dataPoints) + let minValue = DataFunctions.minValue(dataPoints: chartData.dataPoints) let dataPointCount : Int = chartData.dataPoints.count let xSection : CGFloat = chartSize.size.width / CGFloat(dataPointCount - 1) @@ -159,7 +159,7 @@ internal struct TouchOverlay: ViewModifier { let dataPointCount : Int = chartData.dataPoints.count let xSection : CGFloat = chartSize.size.width / CGFloat(dataPointCount) - let ySection : CGFloat = chartSize.size.height / CGFloat(chartData.maxValue()) + let ySection : CGFloat = chartSize.size.height / CGFloat(DataFunctions.maxValue(dataPoints: chartData.dataPoints)) let index = Int((touchLocation.x) / xSection) @@ -210,11 +210,26 @@ internal struct TouchOverlay: ViewModifier { } #endif + +//extension Chart { +// #if !os(tvOS) +// /// 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") -> some View { +// self.modifier(TouchOverlay(specifier: specifier)) +// } +// #elseif os(tvOS) +// public func touchOverlay(specifier: String = "%.0f") -> some View { +// self.modifier(EmptyModifier()) +// } +// #endif +// +//} extension View { #if !os(tvOS) /// 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") -> some View { + public func touchOverlay(specifier: String = "%.0f") -> some View{ self.modifier(TouchOverlay(specifier: specifier)) } #elseif os(tvOS) @@ -224,4 +239,3 @@ extension View { #endif } - diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift index ced46606..c1a04ecd 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift @@ -88,8 +88,9 @@ internal struct YAxisLabels: ViewModifier { internal func getYLabelsLineChart(_ numberOfLabels: Int) -> [Double] { var labels : [Double] = [Double]() - let dataRange : Double = chartData.range() - let minValue : Double = chartData.minValue() + let dataRange : Double = DataFunctions.range(dataPoints: chartData.dataPoints) + let minValue : Double = DataFunctions.minValue(dataPoints: chartData.dataPoints) + let range : Double = dataRange / Double(numberOfLabels) labels.append(minValue) for index in 1...numberOfLabels { @@ -99,8 +100,9 @@ internal struct YAxisLabels: ViewModifier { } internal func getYLabelsBarChart(_ numberOfLabels: Int) -> [Double] { var labels : [Double] = [Double]() + let maxValue : Double = DataFunctions.maxValue(dataPoints: chartData.dataPoints) for index in 0...numberOfLabels { - labels.append(chartData.maxValue() / Double(numberOfLabels) * Double(index)) + labels.append(maxValue / Double(numberOfLabels) * Double(index)) } return labels } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift index bfe508d6..d718479e 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift @@ -39,7 +39,7 @@ internal struct YAxisPOI: ViewModifier { ZStack { content if chartData.isGreaterThanTwo { - Marker(chartData: chartData, markerValue: markerValue, isAverage: isAverage, chartType: chartData.viewData.chartType) + Marker(dataPoints: chartData.dataPoints, markerValue: markerValue, isAverage: isAverage, chartType: chartData.viewData.chartType) .stroke(lineColour, style: strokeStyle) .onAppear { if !chartData.legends.contains(where: { $0.legend == markerName }) { // init twice From 9ddf3f5ef2d1c035ce3d449d6d1c072ced769ff6 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sat, 23 Jan 2021 10:51:34 +0000 Subject: [PATCH 004/152] Adapt data models to use protocol design pattern. --- .../BarChart/Models/BarChartData.swift | 56 ++++ .../BarChart/Models/BarDataSet.swift | 32 +++ .../BarChart/Models/BarStyle.swift | 20 +- .../RoundedRectangleBarShape.swift | 0 .../BarChart/Views/BarChartView.swift | 216 +++++++------- .../LineChart/Models/LineChartData.swift | 73 +++++ .../LineChart/Models/LineDataSet.swift | 33 +++ .../LineChart/Models/LineStyle.swift | 36 +-- .../LineChart/Shapes/LineShape.swift | 27 +- .../LineChart/Views/LineChartView.swift | 267 ++++++++++-------- .../Shared/Extras/Calculations.swift | 256 ++++++++--------- .../Shared/Extras/DataFunctions.swift | 86 ++++++ .../Shared/Models/ChartData.swift | 185 ------------ .../Shared/Models/ChartMetadata.swift | 10 +- .../Shared/Models/ChartViewData.swift | 4 +- .../Shared/Models/LegendData.swift | 8 +- .../Shared/Models/PointStyle.swift | 2 +- .../Shared/Models/Protocols.swift | 42 +++ .../SwiftUICharts/Shared/Shapes/Marker.swift | 34 ++- .../Shared/Shapes/PointShape.swift | 49 ++-- .../Shared/Shapes/TouchOverlayMarker.swift | 4 +- .../Shared/ViewModifiers/AxisBorders.swift | 16 +- .../Shared/ViewModifiers/HeaderBox.swift | 31 +- .../Shared/ViewModifiers/Legends.swift | 8 +- .../Shared/ViewModifiers/PointMarkers.swift | 73 +++-- .../Shared/ViewModifiers/TouchOverlay.swift | 179 ++++++------ .../Shared/ViewModifiers/XAxisGrid.swift | 16 +- .../Shared/ViewModifiers/XAxisLabels.swift | 18 +- .../Shared/ViewModifiers/YAxisGrid.swift | 16 +- .../Shared/ViewModifiers/YAxisLabels.swift | 27 +- .../Shared/ViewModifiers/YAxisPOI.swift | 102 ++++--- .../Shared/Views/CustomNoDataView.swift | 24 +- .../Shared/Views/LegendView.swift | 81 +++--- .../Shared/Views/TouchOverlayBox.swift | 32 ++- 34 files changed, 1148 insertions(+), 915 deletions(-) create mode 100644 Sources/SwiftUICharts/BarChart/Models/BarChartData.swift create mode 100644 Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift rename Sources/SwiftUICharts/BarChart/{ => Shapes}/RoundedRectangleBarShape.swift (100%) create mode 100644 Sources/SwiftUICharts/LineChart/Models/LineChartData.swift create mode 100644 Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift create mode 100644 Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift delete mode 100644 Sources/SwiftUICharts/Shared/Models/ChartData.swift create mode 100644 Sources/SwiftUICharts/Shared/Models/Protocols.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift new file mode 100644 index 00000000..eaa9bf31 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -0,0 +1,56 @@ +// +// File.swift +// +// +// Created by Will Dale on 23/01/2021. +// + +import SwiftUI + +public class BarChartData: ChartData { + + public let id : UUID = UUID() + + @Published public var dataSets : [Set] + @Published public var metadata : ChartMetadata? + @Published public var xAxisLabels : [String]? + @Published public var chartStyle : ChartStyle + @Published public var legends : [LegendData] + @Published public var viewData : ChartViewData + public var noDataText : Text = Text("No Data") + + public init(dataSets : [BarDataSet], + metadata : ChartMetadata? = nil, + xAxisLabels : [String]? = nil, + chartStyle : ChartStyle = ChartStyle(), + calculations: CalculationType = .none + ) { + self.dataSets = dataSets + self.metadata = metadata + self.xAxisLabels = xAxisLabels + self.chartStyle = chartStyle + self.legends = [LegendData]() + self.viewData = ChartViewData() + } + + public init(dataSets : [BarDataSet], + metadata : ChartMetadata? = nil, + xAxisLabels : [String]? = nil, + chartStyle : ChartStyle = ChartStyle(), + customCalc : @escaping ([ChartDataPoint]) -> [ChartDataPoint]? + ) { + self.dataSets = dataSets + self.metadata = metadata + self.xAxisLabels = xAxisLabels + self.chartStyle = chartStyle + self.legends = [LegendData]() + self.viewData = ChartViewData() + } + + public func legendOrder() -> [LegendData] { + return [LegendData]() + } + + public typealias Set = BarDataSet + +} diff --git a/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift new file mode 100644 index 00000000..28d9d58c --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift @@ -0,0 +1,32 @@ +// +// File.swift +// +// +// Created by Will Dale on 23/01/2021. +// + +import SwiftUI + +public struct BarDataSet: DataSet { + + public let id : UUID + public var dataPoints : [ChartDataPoint] + public var legendTitle : String + public var pointStyle : PointStyle + public var style : BarStyle + + public init(dataPoints : [ChartDataPoint], + legendTitle : String, + pointStyle : PointStyle, + style : BarStyle + ) { + self.id = UUID() + self.dataPoints = dataPoints + self.legendTitle = legendTitle + self.pointStyle = pointStyle + self.style = style + } + + public typealias ID = UUID + public typealias Styling = BarStyle +} diff --git a/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift b/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift index 7bd7c083..cee086b4 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift @@ -8,8 +8,8 @@ import SwiftUI /// Model for controlling the aesthetic of the bar chart. -public struct BarStyle { - +public struct BarStyle: Style, Hashable { + /// How much of the available width to use. 0 ..1 var barWidth : CGFloat /// Corner radius of the bar shape. @@ -17,21 +17,21 @@ public struct BarStyle { /// Where to get the colour data from. var colourFrom : ColourFrom /// Type of colour styling for the chart. - var colourType : ColourType + public var colourType : ColourType /// Single Colour - var colour : Color? + public var colour : Color? /// Colours for Gradient - var colours : [Color]? + public var colours : [Color]? /// Colours and Stops for Gradient with stop control - var stops : [GradientStop]? + public var stops : [GradientStop]? /// Start point for Gradient - var startPoint : UnitPoint? + public var startPoint : UnitPoint? /// End point for Gradient - var endPoint : UnitPoint? - + public var endPoint : UnitPoint? +// public var ignoreZero: Bool /// Bar Chart with single colour /// - Parameters: @@ -109,7 +109,7 @@ public struct BarStyle { } /// Corner radius of the bar shape. -public struct CornerRadius { +public struct CornerRadius: Hashable { var top : CGFloat var bottom : CGFloat diff --git a/Sources/SwiftUICharts/BarChart/RoundedRectangleBarShape.swift b/Sources/SwiftUICharts/BarChart/Shapes/RoundedRectangleBarShape.swift similarity index 100% rename from Sources/SwiftUICharts/BarChart/RoundedRectangleBarShape.swift rename to Sources/SwiftUICharts/BarChart/Shapes/RoundedRectangleBarShape.swift diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift b/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift index 924991a4..ed7346e9 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift @@ -7,118 +7,126 @@ import SwiftUI -public struct BarChart: View { - public init() {} - public var body: some View { - BarChartView() - } -} - -internal struct BarChartView: View { +public struct BarChart: View where ChartData: BarChartData { + + @ObservedObject var chartData: ChartData - @EnvironmentObject var chartData: ChartData + let maxValue : Double - internal var body: some View { + public init(chartData: ChartData) { + self.chartData = chartData - let maxValue: Double = DataFunctions.maxValue(dataPoints: chartData.dataPoints) - let style : BarStyle = chartData.barStyle + self.maxValue = DataFunctions.dataSetMaxValue(from: chartData.dataSets) - 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 - { + chartData.viewData.chartType = .bar + } + + public var body: some View { + +// let maxValue: Double = DataFunctions.maxValue(dataPoints: chartData.dataPoints) +// let style : BarStyle = chartData.barStyle + + HStack(spacing: 0) { + ForEach(chartData.dataSets, id: \.self) { dataSet in + ForEach(dataSet.dataPoints, id: \.self) { dataPoint in + ColourBar(dataSet.style.colour!, dataPoint, maxValue, chartData.chartStyle, dataSet.style) + } + } + } + } +} - 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) - - } +// 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) +// 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) +// } +// } - 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)) - } - } - } - } - -} +// .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/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift new file mode 100644 index 00000000..4f87a007 --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -0,0 +1,73 @@ +// +// LineChartData.swift +// +// +// Created by Will Dale on 23/01/2021. +// + +import SwiftUI + +/// The central model from which the chart is drawn. +public class LineChartData: ChartData { + + public let id : UUID = UUID() + + /// Data model containing the datapoints: Value, Label, Description and Date. Individual colouring for bar chart. + @Published public var dataSets : [Set] + + /// 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 + + /// Array of data to populate the chart legend. + @Published public var legends : [LegendData] + + /// Data model to hold data about the Views layout. + @Published public var viewData : ChartViewData + + public var noDataText : Text = Text("No Data") + + public init(dataSets : [LineDataSet], + metadata : ChartMetadata? = nil, + xAxisLabels : [String]? = nil, + chartStyle : ChartStyle = ChartStyle(), + calculations: CalculationType = .none + ) { + + self.dataSets = dataSets + self.metadata = metadata + self.xAxisLabels = xAxisLabels + self.chartStyle = chartStyle + self.legends = [LegendData]() + self.viewData = ChartViewData() + + } + + public init(dataSets : [LineDataSet], + metadata : ChartMetadata? = nil, + xAxisLabels : [String]? = nil, + chartStyle : ChartStyle = ChartStyle(), + customCalc : @escaping ([ChartDataPoint]) -> [ChartDataPoint]? + ) { + self.dataSets = dataSets + self.metadata = metadata + self.xAxisLabels = xAxisLabels + self.chartStyle = chartStyle + self.legends = [LegendData]() + self.viewData = ChartViewData() + } + + + /// Sets the order the Legends are layed out in. + /// - Returns: Ordered array of Legends. + public func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } + + public typealias Set = LineDataSet +} diff --git a/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift new file mode 100644 index 00000000..73293b88 --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift @@ -0,0 +1,33 @@ +// +// LineDataSet.swift +// +// +// Created by Will Dale on 23/01/2021. +// + +import SwiftUI + +public struct LineDataSet: DataSet { + + public let id : UUID + public var dataPoints : [ChartDataPoint] + public var legendTitle : String + public var pointStyle : PointStyle + public var style : Styling + + public init(dataPoints : [ChartDataPoint], + legendTitle : String, + pointStyle : PointStyle = PointStyle(), + style : LineDataSet.Styling + ) { + self.id = UUID() + 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/LineStyle.swift b/Sources/SwiftUICharts/LineChart/Models/LineStyle.swift index d653506b..cce32d0d 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineStyle.swift @@ -6,16 +6,16 @@ // import SwiftUI - + /// Model for controlling the aesthetic of the line chart. -public struct LineStyle { +public struct LineStyle: Style, Hashable { /// Type of colour styling for the chart. public var colourType : ColourType /// Drawing style of the line public var lineType : LineType - public var strokeStyle : StrokeStyle + public var strokeStyle : Stroke /// Single Colour public var colour : Color? @@ -46,12 +46,12 @@ public struct LineStyle { /// - 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), + strokeStyle : Stroke = Stroke(lineWidth: 3, + lineCap: .round, + lineJoin: .round, + miterLimit: 10, + dash: [CGFloat](), + dashPhase: 0), ignoreZero : Bool = false ) { self.colourType = .colour @@ -80,7 +80,7 @@ public struct LineStyle { endPoint : UnitPoint = .trailing, lineType : LineType = .curvedLine, - strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 3, + strokeStyle : Stroke = Stroke(lineWidth: 3, lineCap: .round, lineJoin: .round, miterLimit: 10, @@ -90,7 +90,7 @@ public struct LineStyle { ) { self.colourType = .gradientColour self.lineType = lineType - self.strokeStyle = strokeStyle + self.strokeStyle = strokeStyle self.colour = nil self.stops = nil @@ -114,12 +114,12 @@ public struct LineStyle { endPoint : UnitPoint = .trailing, lineType : LineType = .curvedLine, - strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 3, - lineCap: .round, - lineJoin: .round, - miterLimit: 10, - dash: [CGFloat](), - dashPhase: 0), + strokeStyle : Stroke = Stroke(lineWidth: 3, + lineCap: .round, + lineJoin: .round, + miterLimit: 10, + dash: [CGFloat](), + dashPhase: 0), ignoreZero : Bool = false ) { self.colourType = .gradientStops @@ -131,7 +131,7 @@ public struct LineStyle { self.stops = stops self.startPoint = startPoint self.endPoint = endPoint - + self.ignoreZero = ignoreZero } } diff --git a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift index 32b50b10..da60cec5 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift @@ -9,35 +9,38 @@ import SwiftUI internal struct LineShape: Shape { - private let dataPoints : [ChartDataPoint] - + private let dataSet : LineDataSet /// Drawing style of the line private let lineType : LineType /// If it's to be filled some extra lines need to be drawn private let isFilled : Bool - internal init(dataPoints : [ChartDataPoint], + private let minValue : Double + private let range : Double + + internal init(dataSet : LineDataSet, lineType : LineType, - isFilled : Bool + isFilled : Bool, + minValue : Double, + range : Double ) { - self.dataPoints = dataPoints + self.dataSet = dataSet self.lineType = lineType self.isFilled = isFilled + self.minValue = minValue + self.range = range } internal func path(in rect: CGRect) -> Path { - - let minValue: Double = DataFunctions.minValue(dataPoints: dataPoints) - let range : Double = DataFunctions.range(dataPoints: dataPoints) - - let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1) + + let x : CGFloat = rect.width / CGFloat(dataSet.dataPoints.count - 1) let y : CGFloat = rect.height / CGFloat(range) switch lineType { case .curvedLine: - return curvedLine(rect, x, y, dataPoints, minValue, range, isFilled) + return curvedLine(rect, x, y, dataSet.dataPoints, minValue, range, isFilled) case .line: - return straightLine(rect, x, y, dataPoints, minValue, range, isFilled) + return straightLine(rect, x, y, dataSet.dataPoints, minValue, range, isFilled) } } diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index d575d22d..059d9240 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -7,104 +7,167 @@ import SwiftUI -public struct LineChart: View { +public struct LineChart: View where ChartData: LineChartData { - @EnvironmentObject var chartData: ChartData + @ObservedObject var chartData: ChartData + + private let minValue : Double + private let range : Double @State var startAnimation : Bool = false - public init() {} + public init(chartData: ChartData) { + self.chartData = chartData + self.minValue = DataFunctions.dataSetMinValue(from: chartData.dataSets) + self.range = DataFunctions.dataSetRange(from: chartData.dataSets) + + setupLegends() + } public var body: some View { - let style : LineStyle = chartData.lineStyle - let strokeStyle = style.strokeStyle - if chartData.isGreaterThanTwo { - - if style.colourType == .colour, - let colour = style.colour - { - LineShape(dataPoints: chartData.dataPoints, lineType: style.lineType, isFilled: false) + ZStack { + ForEach(chartData.dataSets, id: \.self) { dataSet in + + let style : LineStyle = dataSet.style + let strokeStyle = style.strokeStyle + +// if chartData.isGreaterThanTwo { + + if style.colourType == .colour, + let colour = style.colour + { + LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) .trim(to: startAnimation ? 1 : 0) - .stroke(colour, style: strokeStyle) + .stroke(colour, style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) .modifier(LineShapeModifiers(chartData)) .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true } - - - } else if style.colourType == .gradientColour, - let colours = style.colours, - let startPoint = style.startPoint, - let endPoint = style.endPoint - { + + } else if style.colourType == .gradientColour, + let colours = style.colours, + let startPoint = style.startPoint, + let endPoint = style.endPoint + { - LineShape(dataPoints: chartData.dataPoints, lineType: style.lineType, isFilled: false) + LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) .trim(to: startAnimation ? 1 : 0) .stroke(LinearGradient(gradient: Gradient(colors: colours), startPoint: startPoint, endPoint: endPoint), - style: strokeStyle) + style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) .modifier(LineShapeModifiers(chartData)) .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true } - - } else if style.colourType == .gradientStops, - let stops = style.stops, - let startPoint = style.startPoint, - let endPoint = style.endPoint - { - let stops = GradientStop.convertToGradientStopsArray(stops: stops) - - LineShape(dataPoints: chartData.dataPoints, lineType: style.lineType, isFilled: false) + + } else if style.colourType == .gradientStops, + let stops = style.stops, + let startPoint = style.startPoint, + let endPoint = style.endPoint + { + let stops = GradientStop.convertToGradientStopsArray(stops: stops) + + LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) .trim(to: startAnimation ? 1 : 0) .stroke(LinearGradient(gradient: Gradient(stops: stops), startPoint: startPoint, endPoint: endPoint), - style: strokeStyle) + style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) .modifier(LineShapeModifiers(chartData)) .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true } + } } - } else { CustomNoDataView(chartData: chartData) } +// } else { CustomNoDataView(chartData: chartData) } + } + } + internal func setupLegends() { + + for dataSet in chartData.dataSets { + if dataSet.style.colourType == .colour, + let colour = dataSet.style.colour + { + let lineDataSet = dataSet as LineDataSet + self.chartData.legends.append(LegendData(legend : dataSet.legendTitle, + colour : colour, + strokeStyle: lineDataSet.style.strokeStyle, + prioity : 1, + chartType : .line)) + + } else if dataSet.style.colourType == .gradientColour, + let colours = dataSet.style.colours + { + let lineDataSet = dataSet as LineDataSet + self.chartData.legends.append(LegendData(legend : dataSet.legendTitle, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: lineDataSet.style.strokeStyle, + prioity : 1, + chartType : .line)) + + } else if dataSet.style.colourType == .gradientStops, + let stops = dataSet.style.stops + { + let lineDataSet = dataSet as LineDataSet + self.chartData.legends.append(LegendData(legend : dataSet.legendTitle, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: lineDataSet.style.strokeStyle, + prioity : 1, + chartType : .line)) + } + } + chartData.viewData.chartType = .line } } -public struct FilledLineChart: View { - - @EnvironmentObject var chartData: ChartData +public struct FilledLineChart: View where ChartData: LineChartData { + + @ObservedObject var chartData: ChartData + + private let minValue : Double + private let range : Double @State var startAnimation : Bool = false - - public init() {} - + + public init(chartData: ChartData) { + self.chartData = chartData + self.minValue = DataFunctions.dataSetMinValue(from: chartData.dataSets) + self.range = DataFunctions.dataSetRange(from: chartData.dataSets) + } + public var body: some View { - - let style : LineStyle = chartData.lineStyle - - if chartData.isGreaterThanTwo { - - if style.colourType == .colour, - let colour = style.colour - { - - LineShape(dataPoints: chartData.dataPoints, lineType: style.lineType, isFilled: true) - .trim(to: startAnimation ? 1 : 0) - .fill(colour) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - - } else if style.colourType == .gradientColour, - let colours = style.colours, - let startPoint = style.startPoint, - let endPoint = style.endPoint - { - - LineShape(dataPoints: chartData.dataPoints, lineType: style.lineType, isFilled: true) + + ZStack { + ForEach(chartData.dataSets, id: \.self) { dataSet in + + let style : LineStyle = dataSet.style + +// if chartData.isGreaterThanTwo { + + if style.colourType == .colour, + let colour = style.colour + { + + LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: true, minValue: minValue, range: range) + .trim(to: startAnimation ? 1 : 0) + .fill(colour) + .modifier(LineShapeModifiers(chartData)) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + } else if style.colourType == .gradientColour, + let colours = style.colours, + let startPoint = style.startPoint, + let endPoint = style.endPoint + { + + LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: true, minValue: minValue, range: range) .trim(to: startAnimation ? 1 : 0) .fill(LinearGradient(gradient: Gradient(colors: colours), startPoint: startPoint, endPoint: endPoint)) .modifier(LineShapeModifiers(chartData)) @@ -112,14 +175,14 @@ public struct FilledLineChart: View { self.startAnimation = true } - } else if style.colourType == .gradientStops, - let stops = style.stops, - let startPoint = style.startPoint, - let endPoint = style.endPoint - { - let stops = GradientStop.convertToGradientStopsArray(stops: stops) - - LineShape(dataPoints: chartData.dataPoints, lineType: style.lineType, isFilled: true) + } else if style.colourType == .gradientStops, + let stops = style.stops, + let startPoint = style.startPoint, + let endPoint = style.endPoint + { + let stops = GradientStop.convertToGradientStopsArray(stops: stops) + + LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: true, minValue: minValue, range: range) .trim(to: startAnimation ? 1 : 0) .fill(LinearGradient(gradient: Gradient(stops: stops), startPoint: startPoint, @@ -129,65 +192,25 @@ public struct FilledLineChart: View { self.startAnimation = true } + } + +// } else { CustomNoDataView(chartData: chartData) } } - } else { CustomNoDataView(chartData: chartData) } + } } } -internal struct LineShapeModifiers: ViewModifier { - - private let chartData : ChartData - - - internal init(_ chartData : ChartData) { - self.chartData = chartData +internal struct LineShapeModifiers: ViewModifier { + private let chartData : T + + internal init(_ chartData : T) { + 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 + .if(chartData.viewData.hasXAxisLabels) { $0.xAxisBorder(chartData: chartData) } + .if(chartData.viewData.hasYAxisLabels) { $0.yAxisBorder(chartData: chartData) } } } diff --git a/Sources/SwiftUICharts/Shared/Extras/Calculations.swift b/Sources/SwiftUICharts/Shared/Extras/Calculations.swift index 99188b49..36604057 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Calculations.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Calculations.swift @@ -7,131 +7,131 @@ 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 numberOfDays = calendar.dateComponents([.day], - from: firstDataPoint, - to: lastDataPoint).day else { return nil } - - var outputData : [ChartDataPoint] = [] - for index in 0...numberOfDays { - 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) - if !average.isNaN { - outputData.append(ChartDataPoint(value: average, - xAxisLabel: formatterForXAxisLabel.string(from: date), - pointLabel: formatterForPointLabel.string(from: date))) - } - } - } - return outputData - } -} +//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 numberOfDays = calendar.dateComponents([.day], +// from: firstDataPoint, +// to: lastDataPoint).day else { return nil } +// +// var outputData : [ChartDataPoint] = [] +// for index in 0...numberOfDays { +// 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) +// if !average.isNaN { +// outputData.append(ChartDataPoint(value: average, +// xAxisLabel: formatterForXAxisLabel.string(from: date), +// pointLabel: formatterForPointLabel.string(from: date))) +// } +// } +// } +// return outputData +// } +//} diff --git a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift new file mode 100644 index 00000000..f01a8918 --- /dev/null +++ b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift @@ -0,0 +1,86 @@ +// +// DataFunctions.swift +// +// +// Created by Will Dale on 23/01/2021. +// + +import Foundation + +struct DataFunctions { + // MARK: - Functions + /// Get the highest value from dataPoints array. + /// - Returns: Highest value. + static func maxValue(dataPoints: [ChartDataPoint]) -> Double { + return dataPoints.max { $0.value < $1.value }?.value ?? 0 + } + /// Get the Lowest value from dataPoints array. + /// - Returns: Lowest value. + static func minValue(dataPoints: [ChartDataPoint]) -> Double { + return dataPoints.min { $0.value < $1.value }?.value ?? 0 + } + /// Get the average of all the dataPoints. + /// - Returns: Average. + static func average(dataPoints: [ChartDataPoint]) -> 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. + static func range(dataPoints: [ChartDataPoint]) -> 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 + } + + static func dataSetMaxValue(from dataSets: [T]) -> Double { + var setHolder : [Double] = [] + for set in dataSets { + setHolder.append(set.dataPoints.max { $0.value < $1.value }?.value ?? 0) + } + return setHolder.max { $0 < $1 } ?? 0 + } + + static func dataSetMinValue(from dataSets: [T]) -> 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 + } + + static func dataSetAverage(from dataSets: [T]) -> 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) + } + + static func dataSetRange(from dataSets: [T]) -> Double { + var setMaxHolder : [Double] = [] + for set in dataSets { + setMaxHolder.append(set.dataPoints.max { $0.value < $1.value }?.value ?? 0) + } + let maxValue = setMaxHolder.max { $0 < $1 } ?? 0 + + var setMinHolder : [Double] = [] + for set in dataSets { + setMinHolder.append(set.dataPoints.min { $0.value < $1.value }?.value ?? 0) + } + let minValue = setMinHolder.min { $0 < $1 } ?? 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 + } +} diff --git a/Sources/SwiftUICharts/Shared/Models/ChartData.swift b/Sources/SwiftUICharts/Shared/Models/ChartData.swift deleted file mode 100644 index 0da8537a..00000000 --- a/Sources/SwiftUICharts/Shared/Models/ChartData.swift +++ /dev/null @@ -1,185 +0,0 @@ -// -// ChartData.swift -// LineChart -// -// Created by Will Dale on 24/12/2020. -// - -import SwiftUI - -public protocol ChartData: ObservableObject, Identifiable { - var dataPoints : [ChartDataPoint] { get set } - - var metadata : ChartMetadata? { get set } - - var xAxisLabels : [String]? { get set } - - var chartStyle : ChartStyle { get set } - - var lineStyle : LineStyle - - var barStyle : BarStyle - var pointStyle : PointStyle - - var legends : [LegendData] - var viewData : ChartViewData - - public var noDataText : Text = Text("No Data") -} - -public protocol Style { - -} - -/// The central model from which the chart is drawn. -public class LineChartData: ChartData { - - 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 - - @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") - - var isGreaterThanTwo: Bool = true - - // 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() - - greaterThanTwo() - } - - // 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() - - greaterThanTwo() - } - - func greaterThanTwo() { - self.isGreaterThanTwo = dataPoints.count > 2 - } - - /// Sets the order the Legends are layed out in. - /// - Returns: Ordered array of Legends. - func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } -} - - -struct DataFunctions { - // MARK: - Functions - /// Get the highest value from dataPoints array. - /// - Returns: Highest value. - static func maxValue(dataPoints: [ChartDataPoint]) -> Double { - return dataPoints.max { $0.value < $1.value }?.value ?? 0 - } - /// Get the Lowest value from dataPoints array. - /// - Returns: Lowest value. - static func minValue(dataPoints: [ChartDataPoint]) -> Double { - return dataPoints.min { $0.value < $1.value }?.value ?? 0 - } - /// Get the average of all the dataPoints. - /// - Returns: Average. - static func average(dataPoints: [ChartDataPoint]) -> 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. - static func range(dataPoints: [ChartDataPoint]) -> 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 - } -} diff --git a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift index 9ee02935..207cd748 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift @@ -13,21 +13,15 @@ public struct ChartMetadata { var title : String? /// The charts subtitle var subtitle : String? - /// The title for the legend - var lineLegend : String? /// Model to hold the metadata for the chart. /// - Parameters: /// - 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 + subtitle : String? = nil ) { self.title = title - self.subtitle = subtitle - self.lineLegend = lineLegend - + self.subtitle = subtitle } } diff --git a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift index bc4d8d5f..e68d2655 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift @@ -8,7 +8,7 @@ import Foundation /// Data model to pass view information internally so the layout can configure its self. -internal struct ChartViewData { +public struct ChartViewData { /// Pass the type of chart being used to view modifiers. var chartType : ChartType = .line @@ -34,7 +34,7 @@ internal struct ChartViewData { Used by TitleBox */ - var touchOverlayInfo : ChartDataPoint? + var touchOverlayInfo : [ChartDataPoint] = [] /** Set specifier of data point readout diff --git a/Sources/SwiftUICharts/Shared/Models/LegendData.swift b/Sources/SwiftUICharts/Shared/Models/LegendData.swift index a661c619..50bf0b8a 100644 --- a/Sources/SwiftUICharts/Shared/Models/LegendData.swift +++ b/Sources/SwiftUICharts/Shared/Models/LegendData.swift @@ -8,7 +8,7 @@ import SwiftUI /// Data model for Legends -internal struct LegendData: Hashable { +public struct LegendData: Hashable { var chartType : ChartType @@ -39,7 +39,7 @@ internal struct LegendData: Hashable { /// - colour: Single Colour /// - strokeStyle: Stroke Style /// - prioity: Used to make sure the charts data legend is first - internal init(legend : String, + public init(legend : String, colour : Color, strokeStyle: Stroke?, prioity : Int, @@ -64,7 +64,7 @@ internal struct LegendData: Hashable { /// - endPoint: End point for Gradient /// - strokeStyle: Stroke Style /// - prioity: Used to make sure the charts data legend is first - internal init(legend : String, + public init(legend : String, colours : [Color], startPoint : UnitPoint, endPoint : UnitPoint, @@ -91,7 +91,7 @@ internal struct LegendData: Hashable { /// - endPoint: End point for Gradient /// - strokeStyle: Stroke Style /// - prioity: Used to make sure the charts data legend is first - internal init(legend : String, + public init(legend : String, stops : [GradientStop], startPoint : UnitPoint, endPoint : UnitPoint, diff --git a/Sources/SwiftUICharts/Shared/Models/PointStyle.swift b/Sources/SwiftUICharts/Shared/Models/PointStyle.swift index 34da8d29..f9f4cd26 100644 --- a/Sources/SwiftUICharts/Shared/Models/PointStyle.swift +++ b/Sources/SwiftUICharts/Shared/Models/PointStyle.swift @@ -8,7 +8,7 @@ import SwiftUI /// Model for controlling the aesthetic of the point markers. -public struct PointStyle { +public struct PointStyle: Hashable { /// Overall size of the mark public var pointSize : CGFloat diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols.swift new file mode 100644 index 00000000..b25ee8ad --- /dev/null +++ b/Sources/SwiftUICharts/Shared/Models/Protocols.swift @@ -0,0 +1,42 @@ +// +// File.swift +// +// +// Created by Will Dale on 23/01/2021. +// + +import SwiftUI + +public protocol ChartData: ObservableObject, Identifiable { + associatedtype Set : DataSet + + var id : UUID { get } + var dataSets : [Set] { get set } + var metadata : ChartMetadata? { get set } + var xAxisLabels : [String]? { get set } + var chartStyle : ChartStyle { get set } + var legends : [LegendData] { get set } + var viewData : ChartViewData { get set } + var noDataText : Text { get set } + + func legendOrder() -> [LegendData] +} + +public protocol DataSet: Hashable, Identifiable { + associatedtype Styling : Style + var id : ID { get } + var dataPoints : [ChartDataPoint] { get set } + var legendTitle : String { get set } + var pointStyle : PointStyle { get set } + var style : Styling { get set } +} + +public protocol Style { + var colourType : ColourType { get set } + var colour : Color? { get set } + var colours : [Color]? { get set } + var stops : [GradientStop]? { get set } + var startPoint : UnitPoint? { get set } + var endPoint : UnitPoint? { get set } +// var ignoreZero : Bool { get set } +} diff --git a/Sources/SwiftUICharts/Shared/Shapes/Marker.swift b/Sources/SwiftUICharts/Shared/Shapes/Marker.swift index 6fc2e486..a63c6b24 100644 --- a/Sources/SwiftUICharts/Shared/Shapes/Marker.swift +++ b/Sources/SwiftUICharts/Shared/Shapes/Marker.swift @@ -8,32 +8,30 @@ import SwiftUI /// Generic line drawn horrizontally across the chart -internal struct Marker: Shape { - - private let dataPoints : [ChartDataPoint] - private let markerValue : Double - private let isAverage : Bool +internal struct Marker: Shape { + private let value : Double private let chartType : ChartType - internal init(dataPoints : [ChartDataPoint], - markerValue : Double = 0, - isAverage : Bool, + let range : Double + let minValue: Double + let maxValue: Double + + internal init(value : Double, + range : Double, + minValue : Double, + maxValue : Double, chartType : ChartType ) { - self.dataPoints = dataPoints - self.markerValue = markerValue - self.isAverage = isAverage - self.chartType = chartType + self.value = value + self.range = range + self.minValue = minValue + self.maxValue = maxValue + self.chartType = chartType } internal func path(in rect: CGRect) -> Path { - - let range : Double = DataFunctions.range(dataPoints: dataPoints) - let minValue: Double = DataFunctions.minValue(dataPoints: dataPoints) - let maxValue: Double = DataFunctions.maxValue(dataPoints: dataPoints) - let value : Double = isAverage ? DataFunctions.average(dataPoints: dataPoints) : markerValue - + var path = Path() let pointY : CGFloat diff --git a/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift b/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift index 0b3e337c..2401f123 100644 --- a/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift +++ b/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift @@ -7,26 +7,32 @@ import SwiftUI -internal struct Point: Shape { +internal struct Point: Shape where T: DataSet { - private let dataPoints : [ChartDataPoint] + private let dataSet : T private let pointSize : CGFloat private let pointType : PointShape - private let cornerSize : Int - private let chartType : ChartType + + private let maxValue : Double + private let minValue : Double + private let range : Double - internal init(dataPoints: [ChartDataPoint], + internal init(dataSet : T, pointSize : CGFloat = 2, pointType : PointShape, - cornerSize: Int = 3, - chartType : ChartType + chartType : ChartType, + maxValue : Double, + minValue : Double, + range : Double ) { - self.dataPoints = dataPoints + self.dataSet = dataSet self.pointSize = pointSize self.pointType = pointType - self.cornerSize = cornerSize self.chartType = chartType + self.maxValue = maxValue + self.minValue = minValue + self.range = range } internal func path(in rect: CGRect) -> Path { @@ -34,14 +40,14 @@ internal struct Point: Shape { switch chartType { case .line: - lineChartDrawPoints(&path, rect, DataFunctions.minValue(dataPoints: dataPoints), DataFunctions.range(dataPoints: dataPoints)) + lineChartDrawPoints(&path, rect, dataSet.dataPoints, minValue, range) case .bar: - barChartDrawPoints(&path, rect, DataFunctions.minValue(dataPoints: dataPoints), DataFunctions.maxValue(dataPoints: dataPoints)) + barChartDrawPoints(&path, rect, dataSet.dataPoints, minValue, maxValue) } return path } - internal func barChartDrawPoints(_ path: inout Path, _ rect: CGRect, _ minValue: Double, _ maxValue: Double) { + internal func barChartDrawPoints(_ path: inout Path, _ rect: CGRect, _ dataPoints: [ChartDataPoint], _ minValue: Double, _ maxValue: Double) { let x = rect.width / CGFloat(dataPoints.count) let y = rect.height / CGFloat(maxValue) @@ -59,7 +65,7 @@ internal struct Point: Shape { } } - internal func lineChartDrawPoints(_ path: inout Path, _ rect: CGRect, _ minValue: Double, _ range: Double) { + internal func lineChartDrawPoints(_ path: inout Path, _ rect: CGRect, _ dataPoints: [ChartDataPoint], _ minValue: Double, _ range: Double) { let x = rect.width / CGFloat(dataPoints.count-1) let y = rect.height / CGFloat(range) @@ -72,7 +78,6 @@ internal struct Point: Shape { height : pointSize) pointSwitch(&path, firstPoint) -// if !chartData.lineStyle.ignoreZero { for index in 1 ..< dataPoints.count - 1 { let pointX : CGFloat = (CGFloat(index) * x) - pointSize / CGFloat(2) let pointY : CGFloat = ((CGFloat(dataPoints[index].value - minValue) * -y) + rect.height) - pointSize / CGFloat(2) @@ -82,19 +87,7 @@ internal struct Point: Shape { 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(dataPoints.count-1) * x) - pointSize / CGFloat(2) let lastPointY : CGFloat = ((CGFloat(dataPoints[dataPoints.count-1].value - minValue) * -y) + rect.height) - pointSize / CGFloat(2) @@ -112,7 +105,7 @@ internal struct Point: Shape { case .square: path.addRect(point) case .roundSquare: - path.addRoundedRect(in: point, cornerSize: CGSize(width: cornerSize, height: cornerSize)) + path.addRoundedRect(in: point, cornerSize: CGSize(width: 3, height: 3)) } } } diff --git a/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift b/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift index b74f5336..31c5cc69 100644 --- a/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift +++ b/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift @@ -16,10 +16,10 @@ internal struct TouchOverlayMarker: Shape { private var position : CGPoint internal init(type : MarkerLineType = .fullWidth, - position : CGPoint + position : HashablePoint ) { self.type = type - self.position = position + self.position = CGPoint(x: position.x, y: position.y) } internal func path(in rect: CGRect) -> Path { diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/AxisBorders.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/AxisBorders.swift index 6abe3ab7..1f604771 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/AxisBorders.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/AxisBorders.swift @@ -7,9 +7,9 @@ import SwiftUI -internal struct XAxisBorder: ViewModifier { +internal struct XAxisBorder: ViewModifier where T: ChartData { - @EnvironmentObject var chartData: ChartData + @ObservedObject var chartData: T @ViewBuilder internal func body(content: Content) -> some View { @@ -37,9 +37,9 @@ internal struct XAxisBorder: ViewModifier { } } -internal struct YAxisBorder: ViewModifier { +internal struct YAxisBorder: ViewModifier where T: ChartData { - @EnvironmentObject var chartData: ChartData + @ObservedObject var chartData: T @ViewBuilder internal func body(content: Content) -> some View { @@ -68,11 +68,11 @@ internal struct YAxisBorder: ViewModifier { } extension View { - internal func xAxisBorder() -> some View { - self.modifier(XAxisBorder()) + internal func xAxisBorder(chartData: T) -> some View { + self.modifier(XAxisBorder(chartData: chartData)) } - internal func yAxisBorder() -> some View { - self.modifier(YAxisBorder()) + internal func yAxisBorder(chartData: T) -> some View { + self.modifier(YAxisBorder(chartData: chartData)) } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index 893c6116..346bddb7 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -7,16 +7,18 @@ import SwiftUI -internal struct HeaderBox: ViewModifier { +internal struct HeaderBox: ViewModifier where T: ChartData { - @EnvironmentObject var chartData: ChartData + @ObservedObject var chartData: T let showTitle : Bool let showSubtitle: Bool - init(showTitle : Bool = true, + init(chartData : T, + showTitle : Bool = true, showSubtitle : Bool = true ) { + self.chartData = chartData self.showTitle = showTitle self.showSubtitle = showSubtitle } @@ -42,17 +44,16 @@ internal struct HeaderBox: ViewModifier { var touchOverlay: some View { VStack(alignment: .trailing) { - if chartData.viewData.isTouchCurrent, let value = chartData.viewData.touchOverlayInfo?.value { - Text("\(value, specifier: chartData.viewData.touchSpecifier)") - .font(.title3) + if chartData.viewData.isTouchCurrent { + ForEach(chartData.viewData.touchOverlayInfo, id: \.self) { info in + Text("\(info.value, specifier: chartData.viewData.touchSpecifier)") + .font(.title3) + Text("\(info.pointDescription ?? "")") + .font(.subheadline) + } } else { Text("") .font(.title3) - } - if chartData.viewData.isTouchCurrent, let label = chartData.viewData.touchOverlayInfo?.pointDescription { - Text("\(label)") - .font(.subheadline) - } else { Text("") .font(.subheadline) } @@ -61,7 +62,7 @@ internal struct HeaderBox: ViewModifier { @ViewBuilder internal func body(content: Content) -> some View { - if chartData.isGreaterThanTwo { +// if chartData.isGreaterThanTwo { #if !os(tvOS) if chartData.chartStyle.infoBoxPlacement == .floating { VStack(alignment: .leading) { @@ -92,14 +93,14 @@ internal struct HeaderBox: ViewModifier { content } #endif - } else { content } +// } else { content } } } extension View { /// Displays the metadata about the chart /// - Returns: Chart title and subtitle. - public func headerBox() -> some View { - self.modifier(HeaderBox()) + public func headerBox(chartData: T) -> some View { + self.modifier(HeaderBox(chartData: chartData)) } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift index 5df61044..26e6b4eb 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift @@ -7,9 +7,9 @@ import SwiftUI -internal struct Legends: ViewModifier { +internal struct Legends: ViewModifier where T: ChartData { - @EnvironmentObject var chartData: ChartData + @ObservedObject var chartData: T internal func body(content: Content) -> some View { VStack { @@ -21,7 +21,7 @@ internal struct Legends: ViewModifier { extension View { /// Displays legends under the chart. /// - Returns: Legends from the charts data and any markers. - public func legends() -> some View { - self.modifier(Legends()) + public func legends(chartData: T) -> some View { + self.modifier(Legends(chartData: chartData)) } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift index b2a5d814..134a657e 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift @@ -7,32 +7,65 @@ import SwiftUI -internal struct PointMarkers: ViewModifier { +internal struct PointMarkers: ViewModifier where T: ChartData { + + @ObservedObject var chartData: T - @EnvironmentObject var chartData: ChartData + private let maxValue : Double + private let minValue : Double + private let range : Double + internal init(chartData : T) { + self.chartData = chartData + self.maxValue = DataFunctions.dataSetMaxValue(from: chartData.dataSets) + self.minValue = DataFunctions.dataSetMinValue(from: chartData.dataSets) + self.range = DataFunctions.dataSetRange(from: chartData.dataSets) + } internal func body(content: Content) -> some View { - let pointStyle = chartData.pointStyle - return ZStack { + ZStack { content - if chartData.isGreaterThanTwo { - switch pointStyle.pointType { + ForEach(chartData.dataSets, id: \.self) { dataSet in +// if chartData.isGreaterThanTwo { + switch dataSet.pointStyle.pointType { case .filled: - Point(dataPoints: chartData.dataPoints, pointSize: pointStyle.pointSize, pointType: pointStyle.pointShape, chartType: chartData.viewData.chartType) - .fill(pointStyle.fillColour) - case .outline: - Point(dataPoints: chartData.dataPoints, pointSize: pointStyle.pointSize, pointType: pointStyle.pointShape, chartType: chartData.viewData.chartType) - .stroke(pointStyle.borderColour, lineWidth: pointStyle.lineWidth) - case .filledOutLine: - Point(dataPoints: chartData.dataPoints, pointSize: pointStyle.pointSize, pointType: pointStyle.pointShape, chartType: chartData.viewData.chartType) - .stroke(pointStyle.borderColour, lineWidth: pointStyle.lineWidth) - .background(Point(dataPoints: chartData.dataPoints, - pointSize: pointStyle.pointSize, - pointType: pointStyle.pointShape, chartType: chartData.viewData.chartType) - .foregroundColor(pointStyle.fillColour) + Point(dataSet : dataSet, + pointSize : dataSet.pointStyle.pointSize, + pointType : dataSet.pointStyle.pointShape, + chartType : chartData.viewData.chartType, + maxValue : maxValue, + minValue : minValue, + range : range) + .fill(dataSet.pointStyle.fillColour) + case .outline: Text("") + Point(dataSet : dataSet, + pointSize : dataSet.pointStyle.pointSize, + pointType : dataSet.pointStyle.pointShape, + chartType : chartData.viewData.chartType, + maxValue : maxValue, + minValue : minValue, + range : range) + .stroke(dataSet.pointStyle.borderColour, lineWidth: dataSet.pointStyle.lineWidth) + case .filledOutLine: Text("") + Point(dataSet : dataSet, + pointSize : dataSet.pointStyle.pointSize, + pointType : dataSet.pointStyle.pointShape, + chartType : chartData.viewData.chartType, + maxValue : maxValue, + minValue : minValue, + range : range) + .stroke(dataSet.pointStyle.borderColour, lineWidth: dataSet.pointStyle.lineWidth) + .background(Point(dataSet : dataSet, + pointSize : dataSet.pointStyle.pointSize, + pointType : dataSet.pointStyle.pointShape, + chartType : chartData.viewData.chartType, + maxValue : maxValue, + minValue : minValue, + range : range) + .foregroundColor(dataSet.pointStyle.fillColour) ) } +// } } } } @@ -41,7 +74,7 @@ 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()) + public func pointMarkers(chartData: T) -> some View { + self.modifier(PointMarkers(chartData: chartData)) } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 09f22027..337e9d09 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -9,9 +9,9 @@ import SwiftUI #if !os(tvOS) /// Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information. -internal struct TouchOverlay: ViewModifier { +internal struct TouchOverlay: ViewModifier where T: ChartData { - @EnvironmentObject var chartData: ChartData + @ObservedObject var chartData: T /// Decimal precision for labels private let specifier : String @@ -22,9 +22,9 @@ internal struct TouchOverlay: ViewModifier { /// Current location of the touch input @State private var touchLocation : CGPoint = CGPoint(x: 0, y: 0) /// The data point closest to the touch input - @State private var selectedPoint : ChartDataPoint? + @State private var selectedPoints : [ChartDataPoint] = [] /// The location for the nearest data point to the touch input - @State private var pointLocation : CGPoint = CGPoint(x: 0, y: 0) + @State private var pointLocations : [HashablePoint] = [HashablePoint(x: 0, y: 0)] /// Frame information of the data point information box @State private var boxFrame : CGRect = CGRect(x: 0, y: 0, width: 0, height: 50) /// Placement of the data point information box @@ -34,9 +34,12 @@ internal struct TouchOverlay: ViewModifier { /// Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information. /// - Parameters: + /// - chartData: /// - specifier: Decimal precision for labels - /// - infoBoxPlacement: Placement of the data point information panel when touch overlay modifier is applied. - internal init(specifier: String) { + internal init(chartData: T, + specifier: String + ) { + self.chartData = chartData self.specifier = specifier } @@ -53,11 +56,19 @@ internal struct TouchOverlay: ViewModifier { switch chartData.viewData.chartType { case .line: - getPointLocationLineChart(touchLocation: touchLocation, chartSize: geo) - getDataPointLineChart(touchLocation: touchLocation, chartSize: geo) + getPointLocationLineChart(dataSets : chartData.dataSets, + touchLocation : touchLocation, + chartSize : geo) + getDataPointLineChart(dataSets : chartData.dataSets, + touchLocation : touchLocation, + chartSize : geo) case .bar: - getPointLocationBarChart(touchLocation: touchLocation, chartSize: geo) - getDataPointBarChart(touchLocation: touchLocation, chartSize: geo) + getPointLocationBarChart(dataSets: chartData.dataSets, + touchLocation: touchLocation, + chartSize: geo) + getDataPointBarChart(dataSets: chartData.dataSets, + touchLocation: touchLocation, + chartSize: geo) } if chartData.chartStyle.infoBoxPlacement == .floating { @@ -65,9 +76,8 @@ internal struct TouchOverlay: ViewModifier { markerLocation.x = setMarkerXLocation(chartSize: geo) markerLocation.y = setMarkerYLocation(chartSize: geo) } else if chartData.chartStyle.infoBoxPlacement == .header { - chartData.chartStyle.infoBoxPlacement = .header chartData.viewData.isTouchCurrent = true - chartData.viewData.touchOverlayInfo = selectedPoint + chartData.viewData.touchOverlayInfo = selectedPoints } } .onEnded { _ in @@ -76,10 +86,12 @@ internal struct TouchOverlay: ViewModifier { } ) if isTouchCurrent { - TouchOverlayMarker(position: pointLocation) - .stroke(Color(.gray), lineWidth: touchMarkerLineWidth) - if chartData.chartStyle.infoBoxPlacement == .floating, let lineChartStyle = chartData.lineStyle { - TouchOverlayBox(selectedPoint: selectedPoint, specifier: specifier, boxFrame: $boxFrame, ignoreZero: lineChartStyle.ignoreZero) + ForEach(pointLocations, id: \.self) { location in + TouchOverlayMarker(position: location) + .stroke(Color(.gray), lineWidth: touchMarkerLineWidth) + } + if chartData.chartStyle.infoBoxPlacement == .floating { + TouchOverlayBox(selectedPoints: selectedPoints, specifier: specifier, boxFrame: $boxFrame) .position(x: boxLocation.x, y: 0 + (boxFrame.height / 2)) } } @@ -88,53 +100,48 @@ internal struct TouchOverlay: ViewModifier { // } else { content } } - // MARK: - Bar Chart + // MARK: - Line Chart /// Gets the nearest data point to the touch location based on the X axis. /// - Parameters: /// - touchLocation: Current location of the touch /// - chartSize: The size of the chart view as the parent view. - internal func getDataPointLineChart(touchLocation: CGPoint, chartSize: GeometryProxy) /* -> ChartDataPoint */ { - let dataPoints : [ChartDataPoint] = chartData.dataPoints - let xSection : CGFloat = chartSize.size.width / CGFloat(dataPoints.count - 1) - let index = Int((touchLocation.x + (xSection / 2)) / xSection) - if index >= 0 && index < dataPoints.count { - self.selectedPoint = dataPoints[index] + internal func getDataPointLineChart(dataSets : [U], + touchLocation : CGPoint, + chartSize : GeometryProxy) { // -> [ChartDataPoint] + var points : [ChartDataPoint] = [] + for dataSet in dataSets { + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) + let index = Int((touchLocation.x + (xSection / 2)) / xSection) + if index >= 0 && index < dataSet.dataPoints.count { + points.append(dataSet.dataPoints[index]) + } } + self.selectedPoints = points } /// Gets the location of the data point in the view. For Line Chart /// - Parameters: /// - touchLocation: Current location of the touch /// - chartSize: The size of the chart view as the parent view. - internal func getPointLocationLineChart(touchLocation: CGPoint, chartSize: GeometryProxy) /* -> CGPoint */ { + internal func getPointLocationLineChart(dataSets: [U], + touchLocation: CGPoint, + chartSize: GeometryProxy) { // -> CGPoint - let range = DataFunctions.range(dataPoints: chartData.dataPoints) - let minValue = DataFunctions.minValue(dataPoints: chartData.dataPoints) + let range = DataFunctions.dataSetRange(from: dataSets) + let minValue = DataFunctions.dataSetMinValue(from: dataSets) - let dataPointCount : Int = chartData.dataPoints.count - let xSection : CGFloat = chartSize.size.width / CGFloat(dataPointCount - 1) - let ySection : CGFloat = chartSize.size.height / CGFloat(range) - let index = Int((touchLocation.x + (xSection / 2)) / xSection) - - if index >= 0 && index < dataPointCount { - if !chartData.lineStyle.ignoreZero { - self.pointLocation = CGPoint(x: CGFloat(index) * xSection, - y: (CGFloat(chartData.dataPoints[index].value - minValue) * -ySection) + chartSize.size.height) - } else { - var pointValue : Double - if chartData.dataPoints[index].value == 0 { - if index > 0 && index < chartData.dataPoints.count - 1 { - // Set data point value as halfway between the previous and next value - pointValue = (chartData.dataPoints[index-1].value + chartData.dataPoints[index+1].value) / 2 - } else { - pointValue = chartData.dataPoints[index].value - } - } else { - pointValue = chartData.dataPoints[index].value - } - self.pointLocation = CGPoint(x: CGFloat(index) * xSection, - y: (CGFloat(pointValue - minValue) * -ySection) + chartSize.size.height) + var locations : [HashablePoint] = [] + for dataSet in dataSets { + + let dataPointCount : Int = dataSet.dataPoints.count + let xSection : CGFloat = chartSize.size.width / CGFloat(dataPointCount - 1) + let ySection : CGFloat = chartSize.size.height / CGFloat(range) + let index = Int((touchLocation.x + (xSection / 2)) / xSection) + if index >= 0 && index < dataPointCount { + locations.append(HashablePoint(x: CGFloat(index) * xSection, + y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.size.height)) } } + self.pointLocations = locations } // MARK: - Bar Chart @@ -142,31 +149,41 @@ internal struct TouchOverlay: ViewModifier { /// - Parameters: /// - touchLocation: Current location of the touch /// - chartSize: The size of the chart view as the parent view. - internal func getDataPointBarChart(touchLocation: CGPoint, chartSize: GeometryProxy) /* -> ChartDataPoint */ { - let dataPoints : [ChartDataPoint] = chartData.dataPoints - let xSection : CGFloat = chartSize.size.width / CGFloat(dataPoints.count) - let index : Int = Int((touchLocation.x) / xSection) - if index >= 0 && index < dataPoints.count { - self.selectedPoint = dataPoints[index] + internal func getDataPointBarChart(dataSets : [U], + touchLocation : CGPoint, + chartSize : GeometryProxy) { // -> [ChartDataPoint] + var points : [ChartDataPoint] = [] + for dataSet in dataSets { + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count) + let index : Int = Int((touchLocation.x) / xSection) + if index >= 0 && index < dataSet.dataPoints.count { + points.append(dataSet.dataPoints[index]) + } } + self.selectedPoints = points } /// Gets the location of the data point in the view. For BarChart /// - Parameters: /// - touchLocation: Current location of the touch /// - chartSize: The size of the chart view as the parent view. - internal func getPointLocationBarChart(touchLocation: CGPoint, chartSize: GeometryProxy) /* -> CGPoint */ { - - let dataPointCount : Int = chartData.dataPoints.count - let xSection : CGFloat = chartSize.size.width / CGFloat(dataPointCount) - let ySection : CGFloat = chartSize.size.height / CGFloat(DataFunctions.maxValue(dataPoints: chartData.dataPoints)) - - let index = Int((touchLocation.x) / xSection) - - if index >= 0 && index < dataPointCount { - self.pointLocation = CGPoint(x: (CGFloat(index) * xSection) + (xSection / 2), - y: (chartSize.size.height - CGFloat(chartData.dataPoints[index].value) * ySection)) + internal func getPointLocationBarChart(dataSets: [U], + touchLocation: CGPoint, + chartSize: GeometryProxy) { // -> CGPoint + var locations : [HashablePoint] = [] + for dataSet in dataSets { + let dataPointCount : Int = dataSet.dataPoints.count + let xSection : CGFloat = chartSize.size.width / CGFloat(dataPointCount) + let ySection : CGFloat = chartSize.size.height / CGFloat(DataFunctions.dataSetMaxValue(from: dataSets)) + + let index = Int((touchLocation.x) / xSection) + + if index >= 0 && index < dataPointCount { + locations.append(HashablePoint(x: (CGFloat(index) * xSection) + (xSection / 2), + y: (chartSize.size.height - CGFloat(dataSet.dataPoints[index].value) * ySection))) + } } + self.pointLocations = locations } // MARK: - Both @@ -210,32 +227,28 @@ internal struct TouchOverlay: ViewModifier { } #endif - -//extension Chart { -// #if !os(tvOS) -// /// 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") -> some View { -// self.modifier(TouchOverlay(specifier: specifier)) -// } -// #elseif os(tvOS) -// public func touchOverlay(specifier: String = "%.0f") -> some View { -// self.modifier(EmptyModifier()) -// } -// #endif -// -//} extension View { #if !os(tvOS) /// 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") -> some View{ - self.modifier(TouchOverlay(specifier: specifier)) + public func touchOverlay(chartData: T, specifier: String = "%.0f") -> some View { + self.modifier(TouchOverlay(chartData: chartData, specifier: specifier)) } #elseif os(tvOS) public func touchOverlay(specifier: String = "%.0f") -> some View { self.modifier(EmptyModifier()) } #endif +} + + +public struct HashablePoint: Hashable { + public let x : CGFloat + public let y : CGFloat + + public init(x: CGFloat, y: CGFloat) { + self.x = x + self.y = y + } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift index b24bb019..7b437fdb 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift @@ -7,13 +7,13 @@ import SwiftUI -internal struct XAxisGrid: ViewModifier { +internal struct XAxisGrid: ViewModifier where T: ChartData { - @EnvironmentObject var chartData : ChartData + @ObservedObject var chartData : T internal func body(content: Content) -> some View { ZStack { - if chartData.isGreaterThanTwo { +// if chartData.isGreaterThanTwo { HStack { ForEach((0...chartData.chartStyle.xAxisGridStyle.numberOfLines), id: \.self) { index in if index != 0 { @@ -24,7 +24,7 @@ internal struct XAxisGrid: ViewModifier { } VerticalGridView(chartData: chartData) } - } +// } content } } @@ -34,15 +34,15 @@ extension View { /** Adds vertical lines along the X axis. */ - public func xAxisGrid() -> some View { - self.modifier(XAxisGrid()) + public func xAxisGrid(chartData: T) -> some View { + self.modifier(XAxisGrid(chartData: chartData)) } } -internal struct VerticalGridView: View { +internal struct VerticalGridView: View where T: ChartData { - var chartData : ChartData + var chartData : T @State var startAnimation : Bool = false diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift index 77e2d87b..fcbb9d66 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift @@ -7,9 +7,9 @@ import SwiftUI -internal struct XAxisLabels: ViewModifier { +internal struct XAxisLabels: ViewModifier where T: ChartData { - @EnvironmentObject var chartData: ChartData + @ObservedObject var chartData: T @ViewBuilder internal var labels: some View { @@ -19,13 +19,14 @@ internal struct XAxisLabels: ViewModifier { // ChartData -> DataPoints -> xAxisLabel switch chartData.viewData.chartType { case .line: + let lineChartData = chartData as! LineChartData HStack(spacing: 0) { - ForEach(chartData.dataPoints, id: \.self) { data in + ForEach(lineChartData.dataSets[0].dataPoints, id: \.self) { data in Text(data.xAxisLabel ?? "") .font(.caption) .lineLimit(1) .minimumScaleFactor(0.5) - if data != chartData.dataPoints[chartData.dataPoints.count - 1] { + if data != lineChartData.dataSets[0].dataPoints[lineChartData.dataSets[0].dataPoints.count - 1] { Spacer() .frame(minWidth: 0, maxWidth: 500) } @@ -35,10 +36,10 @@ internal struct XAxisLabels: ViewModifier { .onAppear { chartData.viewData.hasXAxisLabels = true } - case .bar: + let barChartData = chartData as! BarChartData HStack(spacing: 0) { - ForEach(chartData.dataPoints, id: \.self) { data in + ForEach(barChartData.dataSets[0].dataPoints, id: \.self) { data in Spacer() .frame(minWidth: 0, maxWidth: 500) Text(data.xAxisLabel ?? "") @@ -114,14 +115,13 @@ internal struct XAxisLabels: ViewModifier { content labels } - } } } extension View { /// Labels for the X axis. - public func xAxisLabels() -> some View { - self.modifier(XAxisLabels()) + public func xAxisLabels(chartData: T) -> some View { + self.modifier(XAxisLabels(chartData: chartData)) } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift index 92f403a6..b018670d 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift @@ -7,13 +7,13 @@ import SwiftUI -internal struct YAxisGrid: ViewModifier { +internal struct YAxisGrid: ViewModifier where T: ChartData { - @EnvironmentObject var chartData : ChartData + @ObservedObject var chartData : T internal func body(content: Content) -> some View { ZStack { - if chartData.isGreaterThanTwo { +// if chartData.isGreaterThanTwo { VStack { ForEach((0...chartData.chartStyle.yAxisGridStyle.numberOfLines), id: \.self) { index in if index != 0 { @@ -26,7 +26,7 @@ internal struct YAxisGrid: ViewModifier { } HorizontalGridView(chartData: chartData) } - } +// } content } } @@ -38,15 +38,15 @@ extension View { - Parameter numberOfLines: Number of lines subdividing the chart - Returns: View of evenly spaced horizontal lines */ - public func yAxisGrid() -> some View { - self.modifier(YAxisGrid()) + public func yAxisGrid(chartData: T) -> some View { + self.modifier(YAxisGrid(chartData: chartData)) } } -internal struct HorizontalGridView: View { +internal struct HorizontalGridView: View where T: ChartData { - var chartData : ChartData + var chartData : T @State var startAnimation : Bool = false diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift index c1a04ecd..95048e82 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift @@ -7,14 +7,17 @@ import SwiftUI -internal struct YAxisLabels: ViewModifier { +internal struct YAxisLabels: ViewModifier where T: ChartData { - @EnvironmentObject var chartData: ChartData + @ObservedObject var chartData: T let specifier : String var labelsArray : [Double] { getLabels() } - internal init(specifier: String) { + internal init(chartData: T, + specifier: String + ) { + self.chartData = chartData self.specifier = specifier } @@ -61,17 +64,17 @@ internal struct YAxisLabels: ViewModifier { switch chartData.chartStyle.yAxisLabelPosition { case .leading: HStack { - if chartData.isGreaterThanTwo { +// if chartData.isGreaterThanTwo { labels - } +// } content } case .trailing: HStack { content - if chartData.isGreaterThanTwo { +// if chartData.isGreaterThanTwo { labels - } +// } } } } @@ -88,8 +91,8 @@ internal struct YAxisLabels: ViewModifier { internal func getYLabelsLineChart(_ numberOfLabels: Int) -> [Double] { var labels : [Double] = [Double]() - let dataRange : Double = DataFunctions.range(dataPoints: chartData.dataPoints) - let minValue : Double = DataFunctions.minValue(dataPoints: chartData.dataPoints) + let dataRange : Double = DataFunctions.dataSetRange(from: chartData.dataSets) + let minValue : Double = DataFunctions.dataSetMinValue(from: chartData.dataSets) let range : Double = dataRange / Double(numberOfLabels) labels.append(minValue) @@ -100,7 +103,7 @@ internal struct YAxisLabels: ViewModifier { } internal func getYLabelsBarChart(_ numberOfLabels: Int) -> [Double] { var labels : [Double] = [Double]() - let maxValue : Double = DataFunctions.maxValue(dataPoints: chartData.dataPoints) + let maxValue : Double = DataFunctions.dataSetMaxValue(from: chartData.dataSets) for index in 0...numberOfLabels { labels.append(maxValue / Double(numberOfLabels) * Double(index)) } @@ -115,7 +118,7 @@ extension View { - specifier: Decimal precision specifier - Returns: HStack of labels */ - public func yAxisLabels(specifier: String = "%.0f") -> some View { - self.modifier(YAxisLabels(specifier: specifier)) + public func yAxisLabels(chartData: T, specifier: String = "%.0f") -> some View { + self.modifier(YAxisLabels(chartData: chartData, specifier: specifier)) } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift index d718479e..5356038d 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift @@ -8,48 +8,69 @@ import SwiftUI /// Configurable Point of interest -internal struct YAxisPOI: ViewModifier { +internal struct YAxisPOI: ViewModifier where T: ChartData { - @EnvironmentObject var chartData: ChartData + @ObservedObject var chartData: T private let markerName : String - private let markerValue : Double + private var markerValue : Double private let lineColour : Color - private let strokeStyle : StrokeStyle - private let isAverage : Bool + private let range : Double + private let minValue : Double + private let maxValue : Double - internal init(markerName : String, + internal init(chartData : T, + markerName : String, markerValue : Double = 0, lineColour : Color, strokeStyle : StrokeStyle, isAverage : Bool ) { + self.chartData = chartData self.markerName = markerName self.markerValue = markerValue self.lineColour = lineColour - self.strokeStyle = strokeStyle - self.isAverage = isAverage + self.markerValue = isAverage ? DataFunctions.dataSetAverage(from: chartData.dataSets) : markerValue + //Line + self.range = DataFunctions.dataSetRange(from: chartData.dataSets) + self.minValue = DataFunctions.dataSetMinValue(from: chartData.dataSets) + + + // Bar + /* + + + THIS WILL NEED FIXING !!!! + + + */ + self.maxValue = DataFunctions.maxValue(dataPoints: chartData.dataSets[0].dataPoints) + } internal func body(content: Content) -> some View { ZStack { content - if chartData.isGreaterThanTwo { - Marker(dataPoints: chartData.dataPoints, 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)) - } +// if chartData.isGreaterThanTwo { + Marker(value : markerValue, + range : range, + minValue : minValue, + maxValue : maxValue, + 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)) } +// } } } } @@ -63,18 +84,19 @@ extension View { /// - 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, - lineColour : Color = Color(.blue), - strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 2, - lineCap: .round, - lineJoin: .round, - miterLimit: 10, - dash: [CGFloat](), - dashPhase: 0) - + public func yAxisPOI(chartData : T, + markerName : String, + markerValue : Double, + 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, + self.modifier(YAxisPOI(chartData : chartData, + markerName : markerName, markerValue : markerValue, lineColour : lineColour, strokeStyle : strokeStyle, @@ -88,16 +110,18 @@ extension View { /// - 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", - lineColour : Color = Color.primary, - strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 2, - lineCap: .round, - lineJoin: .round, - miterLimit: 10, - dash: [CGFloat](), - dashPhase: 0) + public func averageLine(chartData : T, + markerName : String = "Average", + 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, + self.modifier(YAxisPOI(chartData : chartData, + markerName : markerName, lineColour : lineColour, strokeStyle : strokeStyle, isAverage : true)) diff --git a/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift b/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift index 4cf8331d..56047e1c 100644 --- a/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift +++ b/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift @@ -7,15 +7,15 @@ import SwiftUI -public struct CustomNoDataView: View { - - let chartData : ChartData - - init(chartData: ChartData) { - self.chartData = chartData - } - - public var body: some View { - chartData.noDataText - } -} +//public struct CustomNoDataView: View { +// +// let chartData : ChartData +// +// init(chartData: ChartData) { +// self.chartData = chartData +// } +// +// public var body: some View { +// chartData.noDataText +// } +//} diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index 235780c0..3516893c 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -7,11 +7,11 @@ import SwiftUI -internal struct LegendView: View { +internal struct LegendView: View where T: ChartData { - @ObservedObject var chartData : ChartData + @ObservedObject var chartData : T - internal init(chartData: ChartData) { + internal init(chartData: T) { self.chartData = chartData } @@ -66,43 +66,44 @@ internal struct LegendView: View { } } 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) - } - } + Text("Hello") +// 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) +// } +// } } } } diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index 8d6a19a9..1ad555ef 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -9,18 +9,18 @@ import SwiftUI internal struct TouchOverlayBox: View { - private var selectedPoint : ChartDataPoint? + private var selectedPoints : [ChartDataPoint] private var specifier : String private var ignoreZero : Bool @Binding private var boxFrame : CGRect - internal init(selectedPoint : ChartDataPoint?, - specifier : String = "%.0f", - boxFrame : Binding, - ignoreZero : Bool + internal init(selectedPoints : [ChartDataPoint], + specifier : String = "%.0f", + boxFrame : Binding, + ignoreZero : Bool = false ) { - self.selectedPoint = selectedPoint + self.selectedPoints = selectedPoints self.specifier = specifier self._boxFrame = boxFrame self.ignoreZero = ignoreZero @@ -28,15 +28,17 @@ internal struct TouchOverlayBox: View { internal var body: some View { VStack { - if ignoreZero && selectedPoint?.value != 0 { - Text("\(selectedPoint?.value ?? 0, specifier: specifier)") - } else if !ignoreZero { - Text("\(selectedPoint?.value ?? 0, specifier: specifier)") - } - if let label = selectedPoint?.pointDescription { - Text(label) - } else if let label = selectedPoint?.xAxisLabel { - Text(label) + ForEach(selectedPoints, id: \.self) { point in + if ignoreZero && point.value != 0 { + Text("\(point.value, specifier: specifier)") + } else if !ignoreZero { + Text("\(point.value, specifier: specifier)") + } + if let label = point.pointDescription { + Text(label) + } else if let label = point.xAxisLabel { + Text(label) + } } } .padding(.all, 8) From f14beb2e48601687e3ba72343f4d1e3bd7798c86 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 24 Jan 2021 11:26:24 +0000 Subject: [PATCH 005/152] Split up types of chart. --- .../BarChart/Models/BarChartData.swift | 59 ++- .../BarChart/Models/BarDataSet.swift | 14 +- .../BarChart/Models/BarStyle.swift | 2 +- .../BarChart/Views/BarChartView.swift | 23 +- .../SwiftUICharts/BarChart/Views/Bars.swift | 8 +- .../LineChart/Models/LineChartData.swift | 22 +- .../LineChart/Models/LineDataSet.swift | 16 +- .../LineChart/Models/LineStyle.swift | 2 +- .../LineChart/Models/MultiLineChartData.swift | 68 +++ .../LineChart/Views/FilledLineChart.swift | 79 +++ .../LineChart/Views/LineChartView.swift | 258 ++++------ .../LineChart/Views/MultiLineChart.swift | 92 ++++ .../Shared/Extras/DataFunctions.swift | 59 ++- .../Shared/Models/Protocols.swift | 43 +- .../Shared/Shapes/PointShape.swift | 204 ++++---- .../Shared/ViewModifiers/HeaderBox.swift | 194 ++++---- .../Shared/ViewModifiers/Legends.swift | 36 +- .../Shared/ViewModifiers/PointMarkers.swift | 140 +++--- .../Shared/ViewModifiers/TouchOverlay.swift | 466 +++++++++--------- .../Shared/ViewModifiers/XAxisLabels.swift | 118 +++-- .../Shared/ViewModifiers/YAxisLabels.swift | 228 ++++----- .../Shared/ViewModifiers/YAxisPOI.swift | 236 ++++----- 22 files changed, 1352 insertions(+), 1015 deletions(-) create mode 100644 Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift create mode 100644 Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift create mode 100644 Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index eaa9bf31..339011c0 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -8,18 +8,19 @@ import SwiftUI public class BarChartData: ChartData { - + public let id : UUID = UUID() - @Published public var dataSets : [Set] + @Published public var dataSets : BarDataSet @Published public var metadata : ChartMetadata? @Published public var xAxisLabels : [String]? @Published public var chartStyle : ChartStyle @Published public var legends : [LegendData] @Published public var viewData : ChartViewData public var noDataText : Text = Text("No Data") + public var chartType: (ChartType, DataSetType) - public init(dataSets : [BarDataSet], + public init(dataSets : BarDataSet, metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, chartStyle : ChartStyle = ChartStyle(), @@ -31,9 +32,10 @@ public class BarChartData: ChartData { self.chartStyle = chartStyle self.legends = [LegendData]() self.viewData = ChartViewData() + self.chartType = (.bar, .single) } - public init(dataSets : [BarDataSet], + public init(dataSets : BarDataSet, metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, chartStyle : ChartStyle = ChartStyle(), @@ -45,12 +47,59 @@ public class BarChartData: ChartData { self.chartStyle = chartStyle self.legends = [LegendData]() self.viewData = ChartViewData() + self.chartType = (.bar, .single) } public func legendOrder() -> [LegendData] { return [LegendData]() } - public typealias Set = BarDataSet +} + +public class MultiBarChartData: ChartData { + + public let id : UUID = UUID() + @Published public var dataSets : MultiBarDataSet + @Published public var metadata : ChartMetadata? + @Published public var xAxisLabels : [String]? + @Published public var chartStyle : ChartStyle + @Published public var legends : [LegendData] + @Published public var viewData : ChartViewData + public var noDataText : Text = Text("No Data") + public var chartType: (ChartType, DataSetType) + + public init(dataSets : MultiBarDataSet, + metadata : ChartMetadata? = nil, + xAxisLabels : [String]? = nil, + chartStyle : ChartStyle = ChartStyle(), + calculations: CalculationType = .none + ) { + self.dataSets = dataSets + self.metadata = metadata + self.xAxisLabels = xAxisLabels + self.chartStyle = chartStyle + self.legends = [LegendData]() + self.viewData = ChartViewData() + self.chartType = (.bar, .single) + } + + public init(dataSets : MultiBarDataSet, + metadata : ChartMetadata? = nil, + xAxisLabels : [String]? = nil, + chartStyle : ChartStyle = ChartStyle(), + customCalc : @escaping ([ChartDataPoint]) -> [ChartDataPoint]? + ) { + self.dataSets = dataSets + self.metadata = metadata + self.xAxisLabels = xAxisLabels + self.chartStyle = chartStyle + self.legends = [LegendData]() + self.viewData = ChartViewData() + self.chartType = (.bar, .single) + } + + public func legendOrder() -> [LegendData] { + return [LegendData]() + } } diff --git a/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift index 28d9d58c..463227b8 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift @@ -7,7 +7,7 @@ import SwiftUI -public struct BarDataSet: DataSet { +public struct BarDataSet: SingleDataSet { public let id : UUID public var dataPoints : [ChartDataPoint] @@ -30,3 +30,15 @@ public struct BarDataSet: DataSet { public typealias ID = UUID public typealias Styling = BarStyle } + +public struct MultiBarDataSet: MultiDataSet { + + public let id : UUID + + public var dataSets : [LineDataSet] + + public init(dataSets: [LineDataSet]) { + self.id = UUID() + self.dataSets = dataSets + } +} diff --git a/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift b/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift index cee086b4..0ac34036 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift @@ -8,7 +8,7 @@ import SwiftUI /// Model for controlling the aesthetic of the bar chart. -public struct BarStyle: Style, Hashable { +public struct BarStyle: CTColourStyle, Hashable { /// How much of the available width to use. 0 ..1 var barWidth : CGFloat diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift b/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift index ed7346e9..06b1f6c0 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift @@ -15,22 +15,15 @@ public struct BarChart: View where ChartData: BarChartData { public init(chartData: ChartData) { self.chartData = chartData - - self.maxValue = DataFunctions.dataSetMaxValue(from: chartData.dataSets) - + self.maxValue = DataFunctions.maxValue(dataPoints: chartData.dataSets.dataPoints) chartData.viewData.chartType = .bar } public var body: some View { - -// let maxValue: Double = DataFunctions.maxValue(dataPoints: chartData.dataPoints) -// let style : BarStyle = chartData.barStyle - + HStack(spacing: 0) { - ForEach(chartData.dataSets, id: \.self) { dataSet in - ForEach(dataSet.dataPoints, id: \.self) { dataPoint in - ColourBar(dataSet.style.colour!, dataPoint, maxValue, chartData.chartStyle, dataSet.style) - } + ForEach(chartData.dataSets.dataPoints) { dataPoint in + ColourBar(chartData.dataSets.style.colour!, dataPoint, maxValue, chartData.chartStyle, chartData.dataSets.style) } } } @@ -64,8 +57,8 @@ public struct BarChart: View where ChartData: BarChartData { // GradientStopsBar(safeStops, startPoint, endPoint, data, maxValue, chartData.chartStyle, style) // // } - - +// +// // case .dataPoints: // if data.colourType == .colour, // let colour = data.colour @@ -90,8 +83,8 @@ public struct BarChart: View where ChartData: BarChartData { // GradientStopsBar(safeStops, startPoint, endPoint, data, maxValue, chartData.chartStyle, style) // } // } - - +// +// // .onAppear { // chartData.viewData.chartType = .bar // diff --git a/Sources/SwiftUICharts/BarChart/Views/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/Bars.swift index 51637bfc..cc7cee0e 100644 --- a/Sources/SwiftUICharts/BarChart/Views/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/Bars.swift @@ -8,13 +8,13 @@ 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, @@ -27,9 +27,9 @@ struct ColourBar: View { 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) diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index 4f87a007..bf6cc811 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -9,11 +9,11 @@ import SwiftUI /// The central model from which the chart is drawn. public class LineChartData: ChartData { - + public let id : UUID = UUID() /// Data model containing the datapoints: Value, Label, Description and Date. Individual colouring for bar chart. - @Published public var dataSets : [Set] + @Published public var dataSets : Set /// Data model containing: the charts Title, the charts Subtitle and the Line Legend. @Published public var metadata : ChartMetadata? @@ -30,25 +30,26 @@ public class LineChartData: ChartData { /// Data model to hold data about the Views layout. @Published public var viewData : ChartViewData - public var noDataText : Text = Text("No Data") + public var noDataText : Text = Text("No Data") + + public var chartType : (ChartType, DataSetType) - public init(dataSets : [LineDataSet], + public init(dataSets : Set, metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, chartStyle : ChartStyle = ChartStyle(), calculations: CalculationType = .none ) { - self.dataSets = dataSets self.metadata = metadata self.xAxisLabels = xAxisLabels self.chartStyle = chartStyle self.legends = [LegendData]() self.viewData = ChartViewData() - + self.chartType = (.line, .single) } - public init(dataSets : [LineDataSet], + public init(dataSets : Set, metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, chartStyle : ChartStyle = ChartStyle(), @@ -60,14 +61,9 @@ public class LineChartData: ChartData { self.chartStyle = chartStyle self.legends = [LegendData]() self.viewData = ChartViewData() + self.chartType = (.line, .single) } - - /// Sets the order the Legends are layed out in. - /// - Returns: Ordered array of Legends. - public func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } public typealias Set = LineDataSet } diff --git a/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift index 73293b88..da4fe4d8 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift @@ -7,7 +7,7 @@ import SwiftUI -public struct LineDataSet: DataSet { +public struct LineDataSet: SingleDataSet { public let id : UUID public var dataPoints : [ChartDataPoint] @@ -31,3 +31,17 @@ public struct LineDataSet: DataSet { public typealias Styling = LineStyle } + + +public struct MultiLineDataSet: MultiDataSet { + + public let id : UUID + + public var dataSets : [LineDataSet] + + public init(dataSets: [LineDataSet]) { + self.id = UUID() + self.dataSets = dataSets + } + +} diff --git a/Sources/SwiftUICharts/LineChart/Models/LineStyle.swift b/Sources/SwiftUICharts/LineChart/Models/LineStyle.swift index cce32d0d..47cfe925 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineStyle.swift @@ -8,7 +8,7 @@ import SwiftUI /// Model for controlling the aesthetic of the line chart. -public struct LineStyle: Style, Hashable { +public struct LineStyle: CTColourStyle, Hashable { /// Type of colour styling for the chart. public var colourType : ColourType diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift new file mode 100644 index 00000000..684dc437 --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -0,0 +1,68 @@ +// +// MultiLineChartData.swift +// +// +// Created by Will Dale on 24/01/2021. +// + +import SwiftUI + +/// The central model from which the chart is drawn. +public class MultiLineChartData: ChartData { + + public let id : UUID = UUID() + + /// Data model containing the datapoints: Value, Label, Description and Date. Individual colouring for bar chart. + @Published public var dataSets : Set + + /// 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 + + /// Array of data to populate the chart legend. + @Published public var legends : [LegendData] + + /// Data model to hold data about the Views layout. + @Published public var viewData : ChartViewData + + public var noDataText : Text = Text("No Data") + + public var chartType : (ChartType, DataSetType) + + public init(dataSets : Set, + metadata : ChartMetadata? = nil, + xAxisLabels : [String]? = nil, + chartStyle : ChartStyle = ChartStyle(), + calculations: CalculationType = .none + ) { + self.dataSets = dataSets + self.metadata = metadata + self.xAxisLabels = xAxisLabels + self.chartStyle = chartStyle + self.legends = [LegendData]() + self.viewData = ChartViewData() + self.chartType = (.line, .multi) + } + + public init(dataSets : Set, + metadata : ChartMetadata? = nil, + xAxisLabels : [String]? = nil, + chartStyle : ChartStyle = ChartStyle(), + customCalc : @escaping ([ChartDataPoint]) -> [ChartDataPoint]? + ) { + self.dataSets = dataSets + self.metadata = metadata + self.xAxisLabels = xAxisLabels + self.chartStyle = chartStyle + self.legends = [LegendData]() + self.viewData = ChartViewData() + self.chartType = (.line, .multi) + } + + public typealias Set = MultiLineDataSet +} diff --git a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift new file mode 100644 index 00000000..1e4c8505 --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift @@ -0,0 +1,79 @@ +// +// FilledLineChart.swift +// +// +// Created by Will Dale on 23/01/2021. +// + +import SwiftUI + +public struct FilledLineChart: View where ChartData: LineChartData { + + @ObservedObject var chartData: ChartData + + private let minValue : Double + private let range : Double + + @State var startAnimation : Bool = false + + public init(chartData: ChartData) { + self.chartData = chartData + self.minValue = DataFunctions.minValue(dataPoints: chartData.dataSets.dataPoints) + self.range = DataFunctions.range(dataPoints: chartData.dataSets.dataPoints) + } + + public var body: some View { + + let style : LineStyle = chartData.dataSets.style + +// if chartData.isGreaterThanTwo { + + if style.colourType == .colour, + let colour = style.colour + { + LineShape(dataSet: chartData.dataSets, lineType: style.lineType, isFilled: true, minValue: minValue, range: range) + .scale(y: startAnimation ? 1 : 0, anchor: .bottom) + .fill(colour) + .modifier(LineShapeModifiers(chartData)) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + } else if style.colourType == .gradientColour, + let colours = style.colours, + let startPoint = style.startPoint, + let endPoint = style.endPoint + { + + LineShape(dataSet: chartData.dataSets, lineType: style.lineType, isFilled: true, minValue: minValue, range: range) + .scale(y: startAnimation ? 1 : 0, anchor: .bottom) + .fill(LinearGradient(gradient: Gradient(colors: colours), + startPoint: startPoint, + endPoint: endPoint)) + .modifier(LineShapeModifiers(chartData)) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + + } else if style.colourType == .gradientStops, + let stops = style.stops, + let startPoint = style.startPoint, + let endPoint = style.endPoint + { + let stops = GradientStop.convertToGradientStopsArray(stops: stops) + + LineShape(dataSet: chartData.dataSets, lineType: style.lineType, isFilled: true, minValue: minValue, range: range) + .scale(y: startAnimation ? 1 : 0, anchor: .bottom) + .fill(LinearGradient(gradient: Gradient(stops: stops), + startPoint: startPoint, + endPoint: endPoint)) + .modifier(LineShapeModifiers(chartData)) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + } +// } else { CustomNoDataView(chartData: chartData) } + } + internal mutating func setupLegends() { + LineLegends.setup(chartData: &chartData, dataSet: chartData.dataSets) + } +} diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index 059d9240..d4d65c68 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -15,192 +15,79 @@ public struct LineChart: View where ChartData: LineChartData { private let range : Double @State var startAnimation : Bool = false - + public init(chartData: ChartData) { self.chartData = chartData - self.minValue = DataFunctions.dataSetMinValue(from: chartData.dataSets) - self.range = DataFunctions.dataSetRange(from: chartData.dataSets) + self.minValue = DataFunctions.minValue(dataPoints: chartData.dataSets.dataPoints) + self.range = DataFunctions.range(dataPoints: chartData.dataSets.dataPoints) setupLegends() } public var body: some View { - ZStack { - ForEach(chartData.dataSets, id: \.self) { dataSet in - - let style : LineStyle = dataSet.style - let strokeStyle = style.strokeStyle - + let style : LineStyle = chartData.dataSets.style + let strokeStyle = style.strokeStyle + // if chartData.isGreaterThanTwo { - - if style.colourType == .colour, - let colour = style.colour - { - LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) - .trim(to: startAnimation ? 1 : 0) - .stroke(colour, style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - - } else if style.colourType == .gradientColour, - let colours = style.colours, - let startPoint = style.startPoint, - let endPoint = style.endPoint - { - - LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) - .trim(to: startAnimation ? 1 : 0) - .stroke(LinearGradient(gradient: Gradient(colors: colours), - startPoint: startPoint, - endPoint: endPoint), - style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - - } else if style.colourType == .gradientStops, - let stops = style.stops, - let startPoint = style.startPoint, - let endPoint = style.endPoint - { - let stops = GradientStop.convertToGradientStopsArray(stops: stops) - - LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) - .trim(to: startAnimation ? 1 : 0) - .stroke(LinearGradient(gradient: Gradient(stops: stops), - startPoint: startPoint, - endPoint: endPoint), - style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } + + if style.colourType == .colour, + let colour = style.colour + { + LineShape(dataSet: chartData.dataSets, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) + .trim(to: startAnimation ? 1 : 0) + .stroke(colour, style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) + .modifier(LineShapeModifiers(chartData)) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true } - } -// } else { CustomNoDataView(chartData: chartData) } - } - } - internal func setupLegends() { - - for dataSet in chartData.dataSets { - if dataSet.style.colourType == .colour, - let colour = dataSet.style.colour - { - let lineDataSet = dataSet as LineDataSet - self.chartData.legends.append(LegendData(legend : dataSet.legendTitle, - colour : colour, - strokeStyle: lineDataSet.style.strokeStyle, - prioity : 1, - chartType : .line)) - - } else if dataSet.style.colourType == .gradientColour, - let colours = dataSet.style.colours - { - let lineDataSet = dataSet as LineDataSet - self.chartData.legends.append(LegendData(legend : dataSet.legendTitle, - colours : colours, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: lineDataSet.style.strokeStyle, - prioity : 1, - chartType : .line)) - - } else if dataSet.style.colourType == .gradientStops, - let stops = dataSet.style.stops - { - let lineDataSet = dataSet as LineDataSet - self.chartData.legends.append(LegendData(legend : dataSet.legendTitle, - stops : stops, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: lineDataSet.style.strokeStyle, - prioity : 1, - chartType : .line)) + } else if style.colourType == .gradientColour, + let colours = style.colours, + let startPoint = style.startPoint, + let endPoint = style.endPoint + { + + LineShape(dataSet: chartData.dataSets, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) + .trim(to: startAnimation ? 1 : 0) + .stroke(LinearGradient(gradient: Gradient(colors: colours), + startPoint: startPoint, + endPoint: endPoint), + style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) + .modifier(LineShapeModifiers(chartData)) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + + } else if style.colourType == .gradientStops, + let stops = style.stops, + let startPoint = style.startPoint, + let endPoint = style.endPoint + { + let stops = GradientStop.convertToGradientStopsArray(stops: stops) + + LineShape(dataSet: chartData.dataSets, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) + .trim(to: startAnimation ? 1 : 0) + .stroke(LinearGradient(gradient: Gradient(stops: stops), + startPoint: startPoint, + endPoint: endPoint), + style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) + .modifier(LineShapeModifiers(chartData)) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true } } - chartData.viewData.chartType = .line +// } else { CustomNoDataView(chartData: chartData) } } -} - -public struct FilledLineChart: View where ChartData: LineChartData { - - @ObservedObject var chartData: ChartData - - private let minValue : Double - private let range : Double - - @State var startAnimation : Bool = false - - public init(chartData: ChartData) { - self.chartData = chartData - self.minValue = DataFunctions.dataSetMinValue(from: chartData.dataSets) - self.range = DataFunctions.dataSetRange(from: chartData.dataSets) + internal mutating func setupLegends() { + LineLegends.setup(chartData: &chartData, dataSet: chartData.dataSets) } +} - public var body: some View { - - ZStack { - ForEach(chartData.dataSets, id: \.self) { dataSet in - - let style : LineStyle = dataSet.style - -// if chartData.isGreaterThanTwo { - - if style.colourType == .colour, - let colour = style.colour - { - - LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: true, minValue: minValue, range: range) - .trim(to: startAnimation ? 1 : 0) - .fill(colour) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - } else if style.colourType == .gradientColour, - let colours = style.colours, - let startPoint = style.startPoint, - let endPoint = style.endPoint - { - - LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: true, minValue: minValue, range: range) - .trim(to: startAnimation ? 1 : 0) - .fill(LinearGradient(gradient: Gradient(colors: colours), startPoint: startPoint, endPoint: endPoint)) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - - } else if style.colourType == .gradientStops, - let stops = style.stops, - let startPoint = style.startPoint, - let endPoint = style.endPoint - { - let stops = GradientStop.convertToGradientStopsArray(stops: stops) - LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: true, minValue: minValue, range: range) - .trim(to: startAnimation ? 1 : 0) - .fill(LinearGradient(gradient: Gradient(stops: stops), - startPoint: startPoint, - endPoint: endPoint)) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - } -// } else { CustomNoDataView(chartData: chartData) } - } - } - } -} -internal struct LineShapeModifiers: ViewModifier { +internal struct LineShapeModifiers: ViewModifier { private let chartData : T internal init(_ chartData : T) { @@ -214,3 +101,40 @@ internal struct LineShapeModifiers: ViewModifier { .if(chartData.viewData.hasYAxisLabels) { $0.yAxisBorder(chartData: chartData) } } } + +struct LineLegends { + static func setup(chartData: inout T, dataSet: LineDataSet) { + if dataSet.style.colourType == .colour, + let colour = dataSet.style.colour + { + chartData.legends.append(LegendData(legend : dataSet.legendTitle, + colour : colour, + strokeStyle: dataSet.style.strokeStyle, + prioity : 1, + chartType : .line)) + + } else if dataSet.style.colourType == .gradientColour, + let colours = dataSet.style.colours + { + chartData.legends.append(LegendData(legend : dataSet.legendTitle, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: dataSet.style.strokeStyle, + prioity : 1, + chartType : .line)) + + } else if dataSet.style.colourType == .gradientStops, + let stops = dataSet.style.stops + { + chartData.legends.append(LegendData(legend : dataSet.legendTitle, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: dataSet.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..3ff82eb1 --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift @@ -0,0 +1,92 @@ +// +// MultiLineChart.swift +// +// +// Created by Will Dale on 23/01/2021. +// + +import SwiftUI + +public struct MultiLineChart: View where ChartData: MultiLineChartData { + + @ObservedObject var chartData: ChartData + + private let minValue : Double + private let range : Double + + @State var startAnimation : Bool = false + + public init(chartData: ChartData) { + self.chartData = chartData + self.minValue = DataFunctions.multiDataSetMinValue(from: chartData.dataSets) + self.range = DataFunctions.multiDataSetRange(from: chartData.dataSets) + + setupLegends() + } + + public var body: some View { + + ZStack { + ForEach(chartData.dataSets.dataSets, id: \.self) { dataSet in + + let style : LineStyle = dataSet.style + let strokeStyle = style.strokeStyle + +// if chartData.isGreaterThanTwo { + + if style.colourType == .colour, + let colour = style.colour + { + LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) + .trim(to: startAnimation ? 1 : 0) + .stroke(colour, style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) + .modifier(LineShapeModifiers(chartData)) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + + } else if style.colourType == .gradientColour, + let colours = style.colours, + let startPoint = style.startPoint, + let endPoint = style.endPoint + { + + LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) + .trim(to: startAnimation ? 1 : 0) + .stroke(LinearGradient(gradient: Gradient(colors: colours), + startPoint: startPoint, + endPoint: endPoint), + style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) + .modifier(LineShapeModifiers(chartData)) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + + } else if style.colourType == .gradientStops, + let stops = style.stops, + let startPoint = style.startPoint, + let endPoint = style.endPoint + { + let stops = GradientStop.convertToGradientStopsArray(stops: stops) + + LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) + .trim(to: startAnimation ? 1 : 0) + .stroke(LinearGradient(gradient: Gradient(stops: stops), + startPoint: startPoint, + endPoint: endPoint), + style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) + .modifier(LineShapeModifiers(chartData)) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + } + } + } +// } else { CustomNoDataView(chartData: chartData) } + } + internal mutating func setupLegends() { + for dataSet in chartData.dataSets.dataSets { + LineLegends.setup(chartData: &chartData, dataSet: dataSet) + } + } +} diff --git a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift index f01a8918..9adf7a19 100644 --- a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift +++ b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift @@ -8,7 +8,8 @@ import Foundation struct DataFunctions { - // MARK: - Functions + + // MARK: - Just DataPoints /// Get the highest value from dataPoints array. /// - Returns: Highest value. static func maxValue(dataPoints: [ChartDataPoint]) -> Double { @@ -38,7 +39,8 @@ struct DataFunctions { return (maxValue - minValue) + 0.001 } - static func dataSetMaxValue(from dataSets: [T]) -> Double { + // MARK: - Single Data Set + static func dataSetMaxValue(from dataSets: [T]) -> Double { var setHolder : [Double] = [] for set in dataSets { setHolder.append(set.dataPoints.max { $0.value < $1.value }?.value ?? 0) @@ -46,7 +48,7 @@ struct DataFunctions { return setHolder.max { $0 < $1 } ?? 0 } - static func dataSetMinValue(from dataSets: [T]) -> Double { + static func dataSetMinValue(from dataSets: [T]) -> Double { var setHolder : [Double] = [] for set in dataSets { setHolder.append(set.dataPoints.min { $0.value < $1.value }?.value ?? 0) @@ -54,7 +56,7 @@ struct DataFunctions { return setHolder.min { $0 < $1 } ?? 0 } - static func dataSetAverage(from dataSets: [T]) -> Double { + static func dataSetAverage(from dataSets: [T]) -> Double { var setHolder : [Double] = [] for set in dataSets { let sum = set.dataPoints.reduce(0) { $0 + $1.value } @@ -64,7 +66,7 @@ struct DataFunctions { return sum / Double(setHolder.count) } - static func dataSetRange(from dataSets: [T]) -> Double { + static func dataSetRange(from dataSets: [T]) -> Double { var setMaxHolder : [Double] = [] for set in dataSets { setMaxHolder.append(set.dataPoints.max { $0.value < $1.value }?.value ?? 0) @@ -83,4 +85,51 @@ struct DataFunctions { */ return (maxValue - minValue) + 0.001 } + + // MARK: - Multi Data Sets + static func multiDataSetMaxValue(from dataSets: T) -> Double { + var setHolder : [Double] = [] + for set in dataSets.dataSets { + setHolder.append(set.dataPoints.max { $0.value < $1.value }?.value ?? 0) + } + return setHolder.max { $0 < $1 } ?? 0 + } + + static func multiDataSetMinValue(from dataSets: T) -> Double { + var setHolder : [Double] = [] + for set in dataSets.dataSets { + setHolder.append(set.dataPoints.min { $0.value < $1.value }?.value ?? 0) + } + return setHolder.min { $0 < $1 } ?? 0 + } + + static func multiDataSetAverage(from dataSets: T) -> Double { + var setHolder : [Double] = [] + for set in dataSets.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) + } + + static func multiDataSetRange(from dataSets: T) -> Double { + var setMaxHolder : [Double] = [] + for set in dataSets.dataSets { + setMaxHolder.append(set.dataPoints.max { $0.value < $1.value }?.value ?? 0) + } + let maxValue = setMaxHolder.max { $0 < $1 } ?? 0 + + var setMinHolder : [Double] = [] + for set in dataSets.dataSets { + setMinHolder.append(set.dataPoints.min { $0.value < $1.value }?.value ?? 0) + } + let minValue = setMinHolder.min { $0 < $1 } ?? 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 + } } diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols.swift index b25ee8ad..bbd88801 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols.swift @@ -9,29 +9,42 @@ import SwiftUI public protocol ChartData: ObservableObject, Identifiable { associatedtype Set : DataSet - var id : UUID { get } - var dataSets : [Set] { get set } + var dataSets : Set { get set } var metadata : ChartMetadata? { get set } var xAxisLabels : [String]? { get set } var chartStyle : ChartStyle { get set } var legends : [LegendData] { get set } var viewData : ChartViewData { get set } var noDataText : Text { get set } - + var chartType : (ChartType, DataSetType) { get } func legendOrder() -> [LegendData] } - +extension ChartData { + /// Sets the order the Legends are layed out in. + /// - Returns: Ordered array of Legends. + public func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } +} public protocol DataSet: Hashable, Identifiable { - associatedtype Styling : Style - var id : ID { get } + var id : ID { get } +} + +public protocol SingleDataSet: DataSet { + associatedtype Styling : CTColourStyle var dataPoints : [ChartDataPoint] { get set } var legendTitle : String { get set } var pointStyle : PointStyle { get set } var style : Styling { get set } } -public protocol Style { +public protocol MultiDataSet: DataSet { + associatedtype DataSet : SingleDataSet + var dataSets : [DataSet] { get set } +} + +public protocol CTColourStyle { var colourType : ColourType { get set } var colour : Color? { get set } var colours : [Color]? { get set } @@ -40,3 +53,19 @@ public protocol Style { var endPoint : UnitPoint? { get set } // var ignoreZero : Bool { get set } } + +public protocol ChartDataPoint: Hashable, Identifiable { + var id : ID { get } + + var value : Double + var xAxisLabel : String? + var pointDescription : String? + var date : Date? +} + + +public enum DataSetType { + case single + case multi +} + diff --git a/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift b/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift index 2401f123..f7927d0f 100644 --- a/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift +++ b/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift @@ -7,105 +7,105 @@ import SwiftUI -internal struct Point: Shape where T: DataSet { - - private let dataSet : T - private let pointSize : CGFloat - private let pointType : PointShape - private let chartType : ChartType - - private let maxValue : Double - private let minValue : Double - private let range : Double - - internal init(dataSet : T, - pointSize : CGFloat = 2, - pointType : PointShape, - chartType : ChartType, - maxValue : Double, - minValue : Double, - range : Double - ) { - self.dataSet = dataSet - self.pointSize = pointSize - self.pointType = pointType - self.chartType = chartType - self.maxValue = maxValue - self.minValue = minValue - self.range = range - } - - internal func path(in rect: CGRect) -> Path { - var path = Path() - - switch chartType { - case .line: - lineChartDrawPoints(&path, rect, dataSet.dataPoints, minValue, range) - case .bar: - barChartDrawPoints(&path, rect, dataSet.dataPoints, minValue, maxValue) - } - return path - } - - internal func barChartDrawPoints(_ path: inout Path, _ rect: CGRect, _ dataPoints: [ChartDataPoint], _ minValue: Double, _ maxValue: Double) { - - let x = rect.width / CGFloat(dataPoints.count) - let y = rect.height / CGFloat(maxValue) - - for index in 0 ..< dataPoints.count { - - let pointX : CGFloat = (CGFloat(index) * x) - (pointSize / CGFloat(2)) + (x / 2) - let pointY : CGFloat = (rect.height - (pointSize / CGFloat(2)) - CGFloat(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, _ dataPoints: [ChartDataPoint], _ minValue: Double, _ range: Double) { - - let x = rect.width / CGFloat(dataPoints.count-1) - let y = rect.height / CGFloat(range) - - let firstPointX : CGFloat = (CGFloat(0) * x) - pointSize / CGFloat(2) - let firstPointY : CGFloat = ((CGFloat(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) - - for index in 1 ..< dataPoints.count - 1 { - let pointX : CGFloat = (CGFloat(index) * x) - pointSize / CGFloat(2) - let pointY : CGFloat = ((CGFloat(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(dataPoints.count-1) * x) - pointSize / CGFloat(2) - let lastPointY : CGFloat = ((CGFloat(dataPoints[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: 3, height: 3)) - } - } -} +//internal struct Point: Shape where T: DataSet { +// +// private let dataSet : T +// private let pointSize : CGFloat +// private let pointType : PointShape +// private let chartType : ChartType +// +// private let maxValue : Double +// private let minValue : Double +// private let range : Double +// +// internal init(dataSet : T, +// pointSize : CGFloat = 2, +// pointType : PointShape, +// chartType : ChartType, +// maxValue : Double, +// minValue : Double, +// range : Double +// ) { +// self.dataSet = dataSet +// self.pointSize = pointSize +// self.pointType = pointType +// self.chartType = chartType +// self.maxValue = maxValue +// self.minValue = minValue +// self.range = range +// } +// +// internal func path(in rect: CGRect) -> Path { +// var path = Path() +// +// switch chartType { +// case .line: +// lineChartDrawPoints(&path, rect, dataSet.dataPoints, minValue, range) +// case .bar: +// barChartDrawPoints(&path, rect, dataSet.dataPoints, minValue, maxValue) +// } +// return path +// } +// +// internal func barChartDrawPoints(_ path: inout Path, _ rect: CGRect, _ dataPoints: [ChartDataPoint], _ minValue: Double, _ maxValue: Double) { +// +// let x = rect.width / CGFloat(dataPoints.count) +// let y = rect.height / CGFloat(maxValue) +// +// for index in 0 ..< dataPoints.count { +// +// let pointX : CGFloat = (CGFloat(index) * x) - (pointSize / CGFloat(2)) + (x / 2) +// let pointY : CGFloat = (rect.height - (pointSize / CGFloat(2)) - CGFloat(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, _ dataPoints: [ChartDataPoint], _ minValue: Double, _ range: Double) { +// +// let x = rect.width / CGFloat(dataPoints.count-1) +// let y = rect.height / CGFloat(range) +// +// let firstPointX : CGFloat = (CGFloat(0) * x) - pointSize / CGFloat(2) +// let firstPointY : CGFloat = ((CGFloat(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) +// +// for index in 1 ..< dataPoints.count - 1 { +// let pointX : CGFloat = (CGFloat(index) * x) - pointSize / CGFloat(2) +// let pointY : CGFloat = ((CGFloat(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(dataPoints.count-1) * x) - pointSize / CGFloat(2) +// let lastPointY : CGFloat = ((CGFloat(dataPoints[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: 3, height: 3)) +// } +// } +//} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index 346bddb7..7b8facce 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -7,100 +7,100 @@ import SwiftUI -internal struct HeaderBox: ViewModifier where T: ChartData { - - @ObservedObject var chartData: T - - let showTitle : Bool - let showSubtitle: Bool - - init(chartData : T, - showTitle : Bool = true, - showSubtitle : Bool = true - ) { - self.chartData = chartData - self.showTitle = showTitle - self.showSubtitle = showSubtitle - } - - 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) - } - } - } - - var touchOverlay: some View { - VStack(alignment: .trailing) { - if chartData.viewData.isTouchCurrent { - ForEach(chartData.viewData.touchOverlayInfo, id: \.self) { info in - Text("\(info.value, specifier: chartData.viewData.touchSpecifier)") - .font(.title3) - Text("\(info.pointDescription ?? "")") - .font(.subheadline) - } - } else { - Text("") - .font(.title3) - Text("") - .font(.subheadline) - } - } - } - - @ViewBuilder - internal func body(content: Content) -> some View { -// if chartData.isGreaterThanTwo { - #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() - HStack(spacing: 0) { - Spacer() - touchOverlay - } - .frame(minWidth: 0, maxWidth: .infinity) - } - content - } - } - #elseif os(tvOS) - VStack(alignment: .leading) { - titleBox - content - } - #endif -// } else { content } - } -} - -extension View { - /// Displays the metadata about the chart - /// - Returns: Chart title and subtitle. - public func headerBox(chartData: T) -> some View { - self.modifier(HeaderBox(chartData: chartData)) - } -} +//internal struct HeaderBox: ViewModifier where T: ChartData { +// +// @ObservedObject var chartData: T +// +// let showTitle : Bool +// let showSubtitle: Bool +// +// init(chartData : T, +// showTitle : Bool = true, +// showSubtitle : Bool = true +// ) { +// self.chartData = chartData +// self.showTitle = showTitle +// self.showSubtitle = showSubtitle +// } +// +// 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) +// } +// } +// } +// +// var touchOverlay: some View { +// VStack(alignment: .trailing) { +// if chartData.viewData.isTouchCurrent { +// ForEach(chartData.viewData.touchOverlayInfo, id: \.self) { info in +// Text("\(info.value, specifier: chartData.viewData.touchSpecifier)") +// .font(.title3) +// Text("\(info.pointDescription ?? "")") +// .font(.subheadline) +// } +// } else { +// Text("") +// .font(.title3) +// Text("") +// .font(.subheadline) +// } +// } +// } +// +// @ViewBuilder +// internal func body(content: Content) -> some View { +//// if chartData.isGreaterThanTwo { +// #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() +// HStack(spacing: 0) { +// Spacer() +// touchOverlay +// } +// .frame(minWidth: 0, maxWidth: .infinity) +// } +// content +// } +// } +// #elseif os(tvOS) +// VStack(alignment: .leading) { +// titleBox +// content +// } +// #endif +//// } else { content } +// } +//} +// +//extension View { +// /// Displays the metadata about the chart +// /// - Returns: Chart title and subtitle. +// public func headerBox(chartData: T) -> some View { +// self.modifier(HeaderBox(chartData: chartData)) +// } +//} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift index 26e6b4eb..2be9ffea 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift @@ -7,21 +7,21 @@ import SwiftUI -internal struct Legends: ViewModifier where T: ChartData { - - @ObservedObject var chartData: T - - internal func body(content: Content) -> some View { - VStack { - content - LegendView(chartData: chartData) - } - } -} -extension View { - /// Displays legends under the chart. - /// - Returns: Legends from the charts data and any markers. - public func legends(chartData: T) -> some View { - self.modifier(Legends(chartData: chartData)) - } -} +//internal struct Legends: ViewModifier where T: ChartData { +// +// @ObservedObject var chartData: T +// +// internal func body(content: Content) -> some View { +// VStack { +// content +// LegendView(chartData: chartData) +// } +// } +//} +//extension View { +// /// Displays legends under the chart. +// /// - Returns: Legends from the charts data and any markers. +// public func legends(chartData: T) -> some View { +// self.modifier(Legends(chartData: chartData)) +// } +//} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift index 134a657e..4e9c6eb9 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift @@ -7,74 +7,74 @@ import SwiftUI -internal struct PointMarkers: ViewModifier where T: ChartData { - - @ObservedObject var chartData: T - - private let maxValue : Double - private let minValue : Double - private let range : Double - - internal init(chartData : T) { - self.chartData = chartData - self.maxValue = DataFunctions.dataSetMaxValue(from: chartData.dataSets) - self.minValue = DataFunctions.dataSetMinValue(from: chartData.dataSets) - self.range = DataFunctions.dataSetRange(from: chartData.dataSets) - } - internal func body(content: Content) -> some View { - - ZStack { - content - ForEach(chartData.dataSets, id: \.self) { dataSet in -// if chartData.isGreaterThanTwo { - switch dataSet.pointStyle.pointType { - case .filled: - Point(dataSet : dataSet, - pointSize : dataSet.pointStyle.pointSize, - pointType : dataSet.pointStyle.pointShape, - chartType : chartData.viewData.chartType, - maxValue : maxValue, - minValue : minValue, - range : range) - .fill(dataSet.pointStyle.fillColour) - case .outline: Text("") - Point(dataSet : dataSet, - pointSize : dataSet.pointStyle.pointSize, - pointType : dataSet.pointStyle.pointShape, - chartType : chartData.viewData.chartType, - maxValue : maxValue, - minValue : minValue, - range : range) - .stroke(dataSet.pointStyle.borderColour, lineWidth: dataSet.pointStyle.lineWidth) - case .filledOutLine: Text("") - Point(dataSet : dataSet, - pointSize : dataSet.pointStyle.pointSize, - pointType : dataSet.pointStyle.pointShape, - chartType : chartData.viewData.chartType, - maxValue : maxValue, - minValue : minValue, - range : range) - .stroke(dataSet.pointStyle.borderColour, lineWidth: dataSet.pointStyle.lineWidth) - .background(Point(dataSet : dataSet, - pointSize : dataSet.pointStyle.pointSize, - pointType : dataSet.pointStyle.pointShape, - chartType : chartData.viewData.chartType, - maxValue : maxValue, - minValue : minValue, - range : range) - .foregroundColor(dataSet.pointStyle.fillColour) - ) - } +//internal struct PointMarkers: ViewModifier where T: ChartData { +// +// @ObservedObject var chartData: T +// +// private let maxValue : Double +// private let minValue : Double +// private let range : Double +// +// internal init(chartData : T) { +// self.chartData = chartData +// self.maxValue = DataFunctions.dataSetMaxValue(from: chartData.dataSets) +// self.minValue = DataFunctions.dataSetMinValue(from: chartData.dataSets) +// self.range = DataFunctions.dataSetRange(from: chartData.dataSets) +// } +// internal func body(content: Content) -> some View { +// +// ZStack { +// content +// ForEach(chartData.dataSets, id: \.self) { dataSet in +//// if chartData.isGreaterThanTwo { +// switch dataSet.pointStyle.pointType { +// case .filled: +// Point(dataSet : dataSet, +// pointSize : dataSet.pointStyle.pointSize, +// pointType : dataSet.pointStyle.pointShape, +// chartType : chartData.viewData.chartType, +// maxValue : maxValue, +// minValue : minValue, +// range : range) +// .fill(dataSet.pointStyle.fillColour) +// case .outline: Text("") +// Point(dataSet : dataSet, +// pointSize : dataSet.pointStyle.pointSize, +// pointType : dataSet.pointStyle.pointShape, +// chartType : chartData.viewData.chartType, +// maxValue : maxValue, +// minValue : minValue, +// range : range) +// .stroke(dataSet.pointStyle.borderColour, lineWidth: dataSet.pointStyle.lineWidth) +// case .filledOutLine: Text("") +// Point(dataSet : dataSet, +// pointSize : dataSet.pointStyle.pointSize, +// pointType : dataSet.pointStyle.pointShape, +// chartType : chartData.viewData.chartType, +// maxValue : maxValue, +// minValue : minValue, +// range : range) +// .stroke(dataSet.pointStyle.borderColour, lineWidth: dataSet.pointStyle.lineWidth) +// .background(Point(dataSet : dataSet, +// pointSize : dataSet.pointStyle.pointSize, +// pointType : dataSet.pointStyle.pointShape, +// chartType : chartData.viewData.chartType, +// maxValue : maxValue, +// minValue : minValue, +// range : range) +// .foregroundColor(dataSet.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(chartData: T) -> some View { - self.modifier(PointMarkers(chartData: chartData)) - } -} +// } +// } +//} +//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(chartData: T) -> some View { +// self.modifier(PointMarkers(chartData: chartData)) +// } +//} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 337e9d09..3f25e4d2 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -7,239 +7,239 @@ import SwiftUI -#if !os(tvOS) -/// Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information. -internal struct TouchOverlay: ViewModifier where T: ChartData { - - @ObservedObject var chartData: T - - /// Decimal precision for labels - private let specifier : String - private let touchMarkerLineWidth : CGFloat = 1 // API? - - /// Boolean that indicates whether touch is currently being detected - @State private var isTouchCurrent : Bool = false - /// Current location of the touch input - @State private var touchLocation : CGPoint = CGPoint(x: 0, y: 0) - /// The data point closest to the touch input - @State private var selectedPoints : [ChartDataPoint] = [] - /// The location for the nearest data point to the touch input - @State private var pointLocations : [HashablePoint] = [HashablePoint(x: 0, y: 0)] - /// Frame information of the data point information box - @State private var boxFrame : CGRect = CGRect(x: 0, y: 0, width: 0, height: 50) - /// Placement of the data point information box - @State private var boxLocation : CGPoint = CGPoint(x: 0, y: 0) - /// Placement of place the markers intersecting the data points location - @State private var markerLocation : CGPoint = CGPoint(x: 0, y: 0) - - /// Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information. - /// - Parameters: - /// - chartData: - /// - specifier: Decimal precision for labels - internal init(chartData: T, - specifier: String - ) { - self.chartData = chartData - self.specifier = specifier - } - - internal func body(content: Content) -> some View { -// if chartData.isGreaterThanTwo { - GeometryReader { geo in - ZStack { - content - .gesture( - DragGesture(minimumDistance: 0) - .onChanged { (value) in - touchLocation = value.location - isTouchCurrent = true - - switch chartData.viewData.chartType { - case .line: - getPointLocationLineChart(dataSets : chartData.dataSets, - touchLocation : touchLocation, - chartSize : geo) - getDataPointLineChart(dataSets : chartData.dataSets, - touchLocation : touchLocation, - chartSize : geo) - case .bar: - getPointLocationBarChart(dataSets: chartData.dataSets, - touchLocation: touchLocation, - chartSize: geo) - getDataPointBarChart(dataSets: chartData.dataSets, - touchLocation: touchLocation, - chartSize: geo) - } - - if chartData.chartStyle.infoBoxPlacement == .floating { - setBoxLocationation(boxFrame: boxFrame, chartSize: geo) - markerLocation.x = setMarkerXLocation(chartSize: geo) - markerLocation.y = setMarkerYLocation(chartSize: geo) - } else if chartData.chartStyle.infoBoxPlacement == .header { - chartData.viewData.isTouchCurrent = true - chartData.viewData.touchOverlayInfo = selectedPoints - } - } - .onEnded { _ in - isTouchCurrent = false - chartData.viewData.isTouchCurrent = false - } - ) - if isTouchCurrent { - ForEach(pointLocations, id: \.self) { location in - TouchOverlayMarker(position: location) - .stroke(Color(.gray), lineWidth: touchMarkerLineWidth) - } - if chartData.chartStyle.infoBoxPlacement == .floating { - TouchOverlayBox(selectedPoints: selectedPoints, specifier: specifier, boxFrame: $boxFrame) - .position(x: boxLocation.x, y: 0 + (boxFrame.height / 2)) - } - } - } - } -// } else { content } - } - - // MARK: - Line Chart - /// Gets the nearest data point to the touch location based on the X axis. - /// - Parameters: - /// - touchLocation: Current location of the touch - /// - chartSize: The size of the chart view as the parent view. - internal func getDataPointLineChart(dataSets : [U], - touchLocation : CGPoint, - chartSize : GeometryProxy) { // -> [ChartDataPoint] - var points : [ChartDataPoint] = [] - for dataSet in dataSets { - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) - let index = Int((touchLocation.x + (xSection / 2)) / xSection) - if index >= 0 && index < dataSet.dataPoints.count { - points.append(dataSet.dataPoints[index]) - } - } - self.selectedPoints = points - } - /// Gets the location of the data point in the view. For Line Chart - /// - Parameters: - /// - touchLocation: Current location of the touch - /// - chartSize: The size of the chart view as the parent view. - internal func getPointLocationLineChart(dataSets: [U], - touchLocation: CGPoint, - chartSize: GeometryProxy) { // -> CGPoint - - let range = DataFunctions.dataSetRange(from: dataSets) - let minValue = DataFunctions.dataSetMinValue(from: dataSets) - - var locations : [HashablePoint] = [] - for dataSet in dataSets { - - let dataPointCount : Int = dataSet.dataPoints.count - let xSection : CGFloat = chartSize.size.width / CGFloat(dataPointCount - 1) - let ySection : CGFloat = chartSize.size.height / CGFloat(range) - let index = Int((touchLocation.x + (xSection / 2)) / xSection) - if index >= 0 && index < dataPointCount { - locations.append(HashablePoint(x: CGFloat(index) * xSection, - y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.size.height)) - } - } - self.pointLocations = locations - } - - // MARK: - Bar Chart - /// Gets the nearest data point to the touch location based on the X axis. - /// - Parameters: - /// - touchLocation: Current location of the touch - /// - chartSize: The size of the chart view as the parent view. - internal func getDataPointBarChart(dataSets : [U], - touchLocation : CGPoint, - chartSize : GeometryProxy) { // -> [ChartDataPoint] - var points : [ChartDataPoint] = [] - for dataSet in dataSets { - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count) - let index : Int = Int((touchLocation.x) / xSection) - if index >= 0 && index < dataSet.dataPoints.count { - points.append(dataSet.dataPoints[index]) - } - } - self.selectedPoints = points - } - - /// Gets the location of the data point in the view. For BarChart - /// - Parameters: - /// - touchLocation: Current location of the touch - /// - chartSize: The size of the chart view as the parent view. - internal func getPointLocationBarChart(dataSets: [U], - touchLocation: CGPoint, - chartSize: GeometryProxy) { // -> CGPoint - var locations : [HashablePoint] = [] - for dataSet in dataSets { - let dataPointCount : Int = dataSet.dataPoints.count - let xSection : CGFloat = chartSize.size.width / CGFloat(dataPointCount) - let ySection : CGFloat = chartSize.size.height / CGFloat(DataFunctions.dataSetMaxValue(from: dataSets)) - - let index = Int((touchLocation.x) / xSection) - - if index >= 0 && index < dataPointCount { - locations.append(HashablePoint(x: (CGFloat(index) * xSection) + (xSection / 2), - y: (chartSize.size.height - CGFloat(dataSet.dataPoints[index].value) * ySection))) - } - } - self.pointLocations = locations - } - - // MARK: - Both - /// Sets the 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(boxFrame: CGRect, chartSize: GeometryProxy) { - if touchLocation.x < chartSize.frame(in: .local).minX + (boxFrame.width / 2) { - boxLocation.x = chartSize.frame(in: .local).minX + (boxFrame.width / 2) - } else if touchLocation.x > chartSize.frame(in: .local).maxX - (boxFrame.width / 2) { - boxLocation.x = chartSize.frame(in: .local).maxX - (boxFrame.width / 2) - } else { - boxLocation.x = touchLocation.x - } - } - /// Sets the X axis marker location while keeping it within the parent view. - /// - Parameter chartSize: The size of the chart view as the parent view. - /// - Returns: Position of the marker. - internal func setMarkerXLocation(chartSize: GeometryProxy) -> CGFloat { - if touchLocation.x < chartSize.frame(in: .local).minX { - return chartSize.frame(in: .local).minX - } else if touchLocation.x > chartSize.frame(in: .local).maxX { - return chartSize.frame(in: .local).maxX - } else { - return touchLocation.x - } - } - /// Sets the Y axis marker location while keeping it within the parent view. - /// - Parameter chartSize: The size of the chart view as the parent view. - /// - Returns: Position of the marker. - internal func setMarkerYLocation(chartSize: GeometryProxy) -> CGFloat { - if touchLocation.y < chartSize.frame(in: .local).minY { - return chartSize.frame(in: .local).minY - } else if touchLocation.y > chartSize.frame(in: .local).maxY { - return chartSize.frame(in: .local).maxY - } else { - return touchLocation.y - } - } -} -#endif - -extension View { - #if !os(tvOS) - /// 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(chartData: T, specifier: String = "%.0f") -> some View { - self.modifier(TouchOverlay(chartData: chartData, specifier: specifier)) - } - #elseif os(tvOS) - public func touchOverlay(specifier: String = "%.0f") -> some View { - self.modifier(EmptyModifier()) - } - #endif -} +//#if !os(tvOS) +///// Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information. +//internal struct TouchOverlay: ViewModifier where T: ChartData { +// +// @ObservedObject var chartData: T +// +// /// Decimal precision for labels +// private let specifier : String +// private let touchMarkerLineWidth : CGFloat = 1 // API? +// +// /// Boolean that indicates whether touch is currently being detected +// @State private var isTouchCurrent : Bool = false +// /// Current location of the touch input +// @State private var touchLocation : CGPoint = CGPoint(x: 0, y: 0) +// /// The data point closest to the touch input +// @State private var selectedPoints : [ChartDataPoint] = [] +// /// The location for the nearest data point to the touch input +// @State private var pointLocations : [HashablePoint] = [HashablePoint(x: 0, y: 0)] +// /// Frame information of the data point information box +// @State private var boxFrame : CGRect = CGRect(x: 0, y: 0, width: 0, height: 50) +// /// Placement of the data point information box +// @State private var boxLocation : CGPoint = CGPoint(x: 0, y: 0) +// /// Placement of place the markers intersecting the data points location +// @State private var markerLocation : CGPoint = CGPoint(x: 0, y: 0) +// +// /// Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information. +// /// - Parameters: +// /// - chartData: +// /// - specifier: Decimal precision for labels +// internal init(chartData: T, +// specifier: String +// ) { +// self.chartData = chartData +// self.specifier = specifier +// } +// +// internal func body(content: Content) -> some View { +//// if chartData.isGreaterThanTwo { +// GeometryReader { geo in +// ZStack { +// content +// .gesture( +// DragGesture(minimumDistance: 0) +// .onChanged { (value) in +// touchLocation = value.location +// isTouchCurrent = true +// +// switch chartData.viewData.chartType { +// case .line: +// getPointLocationLineChart(dataSets : chartData.dataSets, +// touchLocation : touchLocation, +// chartSize : geo) +// getDataPointLineChart(dataSets : chartData.dataSets, +// touchLocation : touchLocation, +// chartSize : geo) +// case .bar: +// getPointLocationBarChart(dataSets: chartData.dataSets, +// touchLocation: touchLocation, +// chartSize: geo) +// getDataPointBarChart(dataSets: chartData.dataSets, +// touchLocation: touchLocation, +// chartSize: geo) +// } +// +// if chartData.chartStyle.infoBoxPlacement == .floating { +// setBoxLocationation(boxFrame: boxFrame, chartSize: geo) +// markerLocation.x = setMarkerXLocation(chartSize: geo) +// markerLocation.y = setMarkerYLocation(chartSize: geo) +// } else if chartData.chartStyle.infoBoxPlacement == .header { +// chartData.viewData.isTouchCurrent = true +// chartData.viewData.touchOverlayInfo = selectedPoints +// } +// } +// .onEnded { _ in +// isTouchCurrent = false +// chartData.viewData.isTouchCurrent = false +// } +// ) +// if isTouchCurrent { +// ForEach(pointLocations, id: \.self) { location in +// TouchOverlayMarker(position: location) +// .stroke(Color(.gray), lineWidth: touchMarkerLineWidth) +// } +// if chartData.chartStyle.infoBoxPlacement == .floating { +// TouchOverlayBox(selectedPoints: selectedPoints, specifier: specifier, boxFrame: $boxFrame) +// .position(x: boxLocation.x, y: 0 + (boxFrame.height / 2)) +// } +// } +// } +// } +//// } else { content } +// } +// +// // MARK: - Line Chart +// /// Gets the nearest data point to the touch location based on the X axis. +// /// - Parameters: +// /// - touchLocation: Current location of the touch +// /// - chartSize: The size of the chart view as the parent view. +// internal func getDataPointLineChart(dataSets : [U], +// touchLocation : CGPoint, +// chartSize : GeometryProxy) { // -> [ChartDataPoint] +// var points : [ChartDataPoint] = [] +// for dataSet in dataSets { +// let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) +// let index = Int((touchLocation.x + (xSection / 2)) / xSection) +// if index >= 0 && index < dataSet.dataPoints.count { +// points.append(dataSet.dataPoints[index]) +// } +// } +// self.selectedPoints = points +// } +// /// Gets the location of the data point in the view. For Line Chart +// /// - Parameters: +// /// - touchLocation: Current location of the touch +// /// - chartSize: The size of the chart view as the parent view. +// internal func getPointLocationLineChart(dataSets: [U], +// touchLocation: CGPoint, +// chartSize: GeometryProxy) { // -> CGPoint +// +// let range = DataFunctions.dataSetRange(from: dataSets) +// let minValue = DataFunctions.dataSetMinValue(from: dataSets) +// +// var locations : [HashablePoint] = [] +// for dataSet in dataSets { +// +// let dataPointCount : Int = dataSet.dataPoints.count +// let xSection : CGFloat = chartSize.size.width / CGFloat(dataPointCount - 1) +// let ySection : CGFloat = chartSize.size.height / CGFloat(range) +// let index = Int((touchLocation.x + (xSection / 2)) / xSection) +// if index >= 0 && index < dataPointCount { +// locations.append(HashablePoint(x: CGFloat(index) * xSection, +// y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.size.height)) +// } +// } +// self.pointLocations = locations +// } +// +// // MARK: - Bar Chart +// /// Gets the nearest data point to the touch location based on the X axis. +// /// - Parameters: +// /// - touchLocation: Current location of the touch +// /// - chartSize: The size of the chart view as the parent view. +// internal func getDataPointBarChart(dataSets : [U], +// touchLocation : CGPoint, +// chartSize : GeometryProxy) { // -> [ChartDataPoint] +// var points : [ChartDataPoint] = [] +// for dataSet in dataSets { +// let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count) +// let index : Int = Int((touchLocation.x) / xSection) +// if index >= 0 && index < dataSet.dataPoints.count { +// points.append(dataSet.dataPoints[index]) +// } +// } +// self.selectedPoints = points +// } +// +// /// Gets the location of the data point in the view. For BarChart +// /// - Parameters: +// /// - touchLocation: Current location of the touch +// /// - chartSize: The size of the chart view as the parent view. +// internal func getPointLocationBarChart(dataSets: [U], +// touchLocation: CGPoint, +// chartSize: GeometryProxy) { // -> CGPoint +// var locations : [HashablePoint] = [] +// for dataSet in dataSets { +// let dataPointCount : Int = dataSet.dataPoints.count +// let xSection : CGFloat = chartSize.size.width / CGFloat(dataPointCount) +// let ySection : CGFloat = chartSize.size.height / CGFloat(DataFunctions.dataSetMaxValue(from: dataSets)) +// +// let index = Int((touchLocation.x) / xSection) +// +// if index >= 0 && index < dataPointCount { +// locations.append(HashablePoint(x: (CGFloat(index) * xSection) + (xSection / 2), +// y: (chartSize.size.height - CGFloat(dataSet.dataPoints[index].value) * ySection))) +// } +// } +// self.pointLocations = locations +// } +// +// // MARK: - Both +// /// Sets the 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(boxFrame: CGRect, chartSize: GeometryProxy) { +// if touchLocation.x < chartSize.frame(in: .local).minX + (boxFrame.width / 2) { +// boxLocation.x = chartSize.frame(in: .local).minX + (boxFrame.width / 2) +// } else if touchLocation.x > chartSize.frame(in: .local).maxX - (boxFrame.width / 2) { +// boxLocation.x = chartSize.frame(in: .local).maxX - (boxFrame.width / 2) +// } else { +// boxLocation.x = touchLocation.x +// } +// } +// /// Sets the X axis marker location while keeping it within the parent view. +// /// - Parameter chartSize: The size of the chart view as the parent view. +// /// - Returns: Position of the marker. +// internal func setMarkerXLocation(chartSize: GeometryProxy) -> CGFloat { +// if touchLocation.x < chartSize.frame(in: .local).minX { +// return chartSize.frame(in: .local).minX +// } else if touchLocation.x > chartSize.frame(in: .local).maxX { +// return chartSize.frame(in: .local).maxX +// } else { +// return touchLocation.x +// } +// } +// /// Sets the Y axis marker location while keeping it within the parent view. +// /// - Parameter chartSize: The size of the chart view as the parent view. +// /// - Returns: Position of the marker. +// internal func setMarkerYLocation(chartSize: GeometryProxy) -> CGFloat { +// if touchLocation.y < chartSize.frame(in: .local).minY { +// return chartSize.frame(in: .local).minY +// } else if touchLocation.y > chartSize.frame(in: .local).maxY { +// return chartSize.frame(in: .local).maxY +// } else { +// return touchLocation.y +// } +// } +//} +//#endif +// +//extension View { +// #if !os(tvOS) +// /// 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(chartData: T, specifier: String = "%.0f") -> some View { +// self.modifier(TouchOverlay(chartData: chartData, specifier: specifier)) +// } +// #elseif os(tvOS) +// public func touchOverlay(specifier: String = "%.0f") -> some View { +// self.modifier(EmptyModifier()) +// } +// #endif +//} public struct HashablePoint: Hashable { diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift index fcbb9d66..10a8cb0d 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift @@ -19,40 +19,71 @@ internal struct XAxisLabels: ViewModifier where T: ChartData { // ChartData -> DataPoints -> xAxisLabel switch chartData.viewData.chartType { case .line: - let lineChartData = chartData as! LineChartData - HStack(spacing: 0) { - ForEach(lineChartData.dataSets[0].dataPoints, id: \.self) { data in - Text(data.xAxisLabel ?? "") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) - if data != lineChartData.dataSets[0].dataPoints[lineChartData.dataSets[0].dataPoints.count - 1] { - Spacer() - .frame(minWidth: 0, maxWidth: 500) + Text("") + if chartData.chartType == (.line, .multi) { + + let lineChartData = chartData as! MultiLineChartData + let dataSet = lineChartData.dataSets.dataSets + + HStack(spacing: 0) { + ForEach(dataSet[0].dataPoints) { data in + Text(data.xAxisLabel ?? "") + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + if data != dataSet[0].dataPoints[dataSet[0].dataPoints.count - 1] { + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } } } - } - .padding(.horizontal, -4) - .onAppear { - chartData.viewData.hasXAxisLabels = true - } - case .bar: - let barChartData = chartData as! BarChartData - HStack(spacing: 0) { - ForEach(barChartData.dataSets[0].dataPoints, id: \.self) { data in - Spacer() - .frame(minWidth: 0, maxWidth: 500) + .padding(.horizontal, -4) + .onAppear { + chartData.viewData.hasXAxisLabels = true + } + + } else if chartData.chartType == (.line, .single) { + + let lineChartData = chartData as! LineChartData + let dataSet = lineChartData.dataSets + + HStack(spacing: 0) { + ForEach(dataSet.dataPoints) { data in Text(data.xAxisLabel ?? "") .font(.caption) .lineLimit(1) .minimumScaleFactor(0.5) - Spacer() - .frame(minWidth: 0, maxWidth: 500) + if data != dataSet.dataPoints[dataSet.dataPoints.count - 1] { + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } + .padding(.horizontal, -4) + .onAppear { + chartData.viewData.hasXAxisLabels = true } } - .onAppear { - chartData.viewData.hasXAxisLabels = true - } + + + case .bar: + Text("hello") +// let barChartData = chartData as! BarChartData +// HStack(spacing: 0) { +// ForEach(barChartData.dataSets[0].dataPoints, id: \.self) { data in +// Spacer() +// .frame(minWidth: 0, maxWidth: 500) +// Text(data.xAxisLabel ?? "") +// .font(.caption) +// .lineLimit(1) +// .minimumScaleFactor(0.5) +// Spacer() +// .frame(minWidth: 0, maxWidth: 500) +// } +// } +// .onAppear { +// chartData.viewData.hasXAxisLabels = true +// } } @@ -80,23 +111,24 @@ internal struct XAxisLabels: ViewModifier where T: ChartData { } } 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 - } - } + Text("Hello") +// 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 +// } +// } } } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift index 95048e82..ebbd7339 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift @@ -7,118 +7,118 @@ import SwiftUI -internal struct YAxisLabels: ViewModifier where T: ChartData { - - @ObservedObject var chartData: T - - let specifier : String - var labelsArray : [Double] { getLabels() } - - internal init(chartData: T, - specifier: String - ) { - self.chartData = chartData - 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.isGreaterThanTwo { - labels -// } - content - } - case .trailing: - HStack { - content -// if chartData.isGreaterThanTwo { - labels +//internal struct YAxisLabels: ViewModifier where T: ChartData { +// +// @ObservedObject var chartData: T +// +// let specifier : String +// var labelsArray : [Double] { getLabels() } +// +// internal init(chartData: T, +// specifier: String +// ) { +// self.chartData = chartData +// 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) // } - } - } - } - - 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] { - var labels : [Double] = [Double]() - let dataRange : Double = DataFunctions.dataSetRange(from: chartData.dataSets) - let minValue : Double = DataFunctions.dataSetMinValue(from: chartData.dataSets) - - let range : Double = dataRange / Double(numberOfLabels) - labels.append(minValue) - for index in 1...numberOfLabels { - labels.append(minValue + range * Double(index)) - } - return labels - } - internal func getYLabelsBarChart(_ numberOfLabels: Int) -> [Double] { - var labels : [Double] = [Double]() - let maxValue : Double = DataFunctions.dataSetMaxValue(from: chartData.dataSets) - for index in 0...numberOfLabels { - labels.append(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(chartData: T, specifier: String = "%.0f") -> some View { - self.modifier(YAxisLabels(chartData: chartData, specifier: specifier)) - } -} +// } +// 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.isGreaterThanTwo { +// labels +//// } +// content +// } +// case .trailing: +// HStack { +// content +//// if chartData.isGreaterThanTwo { +// 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] { +// var labels : [Double] = [Double]() +// let dataRange : Double = DataFunctions.dataSetRange(from: chartData.dataSets) +// let minValue : Double = DataFunctions.dataSetMinValue(from: chartData.dataSets) +// +// let range : Double = dataRange / Double(numberOfLabels) +// labels.append(minValue) +// for index in 1...numberOfLabels { +// labels.append(minValue + range * Double(index)) +// } +// return labels +// } +// internal func getYLabelsBarChart(_ numberOfLabels: Int) -> [Double] { +// var labels : [Double] = [Double]() +// let maxValue : Double = DataFunctions.dataSetMaxValue(from: chartData.dataSets) +// for index in 0...numberOfLabels { +// labels.append(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(chartData: T, specifier: String = "%.0f") -> some View { +// self.modifier(YAxisLabels(chartData: chartData, specifier: specifier)) +// } +//} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift index 5356038d..3a485b1b 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift @@ -8,122 +8,122 @@ import SwiftUI /// Configurable Point of interest -internal struct YAxisPOI: ViewModifier where T: ChartData { - - @ObservedObject var chartData: T - - private let markerName : String - private var markerValue : Double - private let lineColour : Color - private let strokeStyle : StrokeStyle - - private let range : Double - private let minValue : Double - private let maxValue : Double - - internal init(chartData : T, - markerName : String, - markerValue : Double = 0, - lineColour : Color, - strokeStyle : StrokeStyle, - isAverage : Bool - ) { - self.chartData = chartData - self.markerName = markerName - self.markerValue = markerValue - self.lineColour = lineColour - self.strokeStyle = strokeStyle - - self.markerValue = isAverage ? DataFunctions.dataSetAverage(from: chartData.dataSets) : markerValue - //Line - self.range = DataFunctions.dataSetRange(from: chartData.dataSets) - self.minValue = DataFunctions.dataSetMinValue(from: chartData.dataSets) - - - // Bar - /* - - - THIS WILL NEED FIXING !!!! - - - */ - self.maxValue = DataFunctions.maxValue(dataPoints: chartData.dataSets[0].dataPoints) - - } - - internal func body(content: Content) -> some View { - ZStack { - content -// if chartData.isGreaterThanTwo { - Marker(value : markerValue, - range : range, - minValue : minValue, - maxValue : maxValue, - 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)) - } +//internal struct YAxisPOI: ViewModifier where T: ChartData { +// +// @ObservedObject var chartData: T +// +// private let markerName : String +// private var markerValue : Double +// private let lineColour : Color +// private let strokeStyle : StrokeStyle +// +// private let range : Double +// private let minValue : Double +// private let maxValue : Double +// +// internal init(chartData : T, +// markerName : String, +// markerValue : Double = 0, +// lineColour : Color, +// strokeStyle : StrokeStyle, +// isAverage : Bool +// ) { +// self.chartData = chartData +// self.markerName = markerName +// self.markerValue = markerValue +// self.lineColour = lineColour +// self.strokeStyle = strokeStyle +// +// self.markerValue = isAverage ? DataFunctions.dataSetAverage(from: chartData.dataSets) : markerValue +// //Line +// self.range = DataFunctions.dataSetRange(from: chartData.dataSets) +// self.minValue = DataFunctions.dataSetMinValue(from: chartData.dataSets) +// +// +// // Bar +// /* +// +// +// THIS WILL NEED FIXING !!!! +// +// +// */ +// self.maxValue = DataFunctions.maxValue(dataPoints: chartData.dataSets[0].dataPoints) +// +// } +// +// internal func body(content: Content) -> some View { +// ZStack { +// content +//// if chartData.isGreaterThanTwo { +// Marker(value : markerValue, +// range : range, +// minValue : minValue, +// maxValue : maxValue, +// 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)) // } - } - } - } -} - -extension View { - /// Shows a marker line at chosen point. - /// - Parameters: - /// - markerName: Title of marker, for the legend - /// - markerValue : Chosen point. - /// - lineColour: Line Colour - /// - strokeStyle: Style of Stroke - /// - Returns: A marker line at the average of all the data points. - public func yAxisPOI(chartData : T, - markerName : String, - markerValue : Double, - 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, - 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 - /// - lineColour: Line Colour - /// - strokeStyle: Style of Stroke - /// - Returns: A marker line at the average of all the data points. - public func averageLine(chartData : T, - markerName : String = "Average", - 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, - lineColour : lineColour, - strokeStyle : strokeStyle, - isAverage : true)) - } -} +//// } +// } +// } +// } +//} +// +//extension View { +// /// Shows a marker line at chosen point. +// /// - Parameters: +// /// - markerName: Title of marker, for the legend +// /// - markerValue : Chosen point. +// /// - lineColour: Line Colour +// /// - strokeStyle: Style of Stroke +// /// - Returns: A marker line at the average of all the data points. +// public func yAxisPOI(chartData : T, +// markerName : String, +// markerValue : Double, +// 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, +// 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 +// /// - lineColour: Line Colour +// /// - strokeStyle: Style of Stroke +// /// - Returns: A marker line at the average of all the data points. +// public func averageLine(chartData : T, +// markerName : String = "Average", +// 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, +// lineColour : lineColour, +// strokeStyle : strokeStyle, +// isAverage : true)) +// } +//} From 834bf215742eac971640d2c3c0903c33da0909a2 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 24 Jan 2021 11:56:11 +0000 Subject: [PATCH 006/152] Make DataPoint into protocol. --- .../BarChart/Models/BarChartData.swift | 10 ++-- .../Models/BarChartDataPoint.swift} | 9 ++-- .../BarChart/Models/BarDataSet.swift | 4 +- .../SwiftUICharts/BarChart/Views/Bars.swift | 12 ++--- .../LineChart/Models/LineChartData.swift | 4 +- .../LineChart/Models/LineChartDataPoint.swift | 40 +++++++++++++++ .../LineChart/Models/LineDataSet.swift | 4 +- .../LineChart/Models/MultiLineChartData.swift | 4 +- .../LineChart/Shapes/LineShape.swift | 6 +-- .../Shared/Extras/DataFunctions.swift | 8 +-- .../Shared/Models/ChartViewData.swift | 6 +-- .../Shared/Models/LegendData.swift | 51 ++++++++++--------- .../Shared/Models/Protocols.swift | 17 ++++--- .../Shared/Views/TouchOverlayBox.swift | 6 +-- 14 files changed, 113 insertions(+), 68 deletions(-) rename Sources/SwiftUICharts/{Shared/Models/ChartDataPoints.swift => BarChart/Models/BarChartDataPoint.swift} (97%) create mode 100644 Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index 339011c0..63502c36 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -16,7 +16,7 @@ public class BarChartData: ChartData { @Published public var xAxisLabels : [String]? @Published public var chartStyle : ChartStyle @Published public var legends : [LegendData] - @Published public var viewData : ChartViewData + @Published public var viewData : ChartViewData public var noDataText : Text = Text("No Data") public var chartType: (ChartType, DataSetType) @@ -39,7 +39,7 @@ public class BarChartData: ChartData { metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, chartStyle : ChartStyle = ChartStyle(), - customCalc : @escaping ([ChartDataPoint]) -> [ChartDataPoint]? + customCalc : @escaping ([BarChartDataPoint]) -> [BarChartDataPoint]? ) { self.dataSets = dataSets self.metadata = metadata @@ -65,9 +65,9 @@ public class MultiBarChartData: ChartData { @Published public var xAxisLabels : [String]? @Published public var chartStyle : ChartStyle @Published public var legends : [LegendData] - @Published public var viewData : ChartViewData + @Published public var viewData : ChartViewData public var noDataText : Text = Text("No Data") - public var chartType: (ChartType, DataSetType) + public var chartType : (ChartType, DataSetType) public init(dataSets : MultiBarDataSet, metadata : ChartMetadata? = nil, @@ -88,7 +88,7 @@ public class MultiBarChartData: ChartData { metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, chartStyle : ChartStyle = ChartStyle(), - customCalc : @escaping ([ChartDataPoint]) -> [ChartDataPoint]? + customCalc : @escaping ([BarChartDataPoint]) -> [BarChartDataPoint]? ) { self.dataSets = dataSets self.metadata = metadata diff --git a/Sources/SwiftUICharts/Shared/Models/ChartDataPoints.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift similarity index 97% rename from Sources/SwiftUICharts/Shared/Models/ChartDataPoints.swift rename to Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift index f70df638..ef24f489 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartDataPoints.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift @@ -1,14 +1,14 @@ // -// ChartDataPoints.swift -// LineChart +// BarChartDataPoint.swift +// // -// Created by Will Dale on 02/01/2021. +// Created by Will Dale on 24/01/2021. // import SwiftUI /// Data model for a data point. -public struct ChartDataPoint: Hashable, Identifiable { +public struct BarChartDataPoint: ChartDataPoint, CTColourStyle { public let id = UUID() @@ -124,3 +124,4 @@ public struct ChartDataPoint: Hashable, Identifiable { self.colourType = .gradientStops } } + diff --git a/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift index 463227b8..b2cd8469 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift @@ -10,12 +10,12 @@ import SwiftUI public struct BarDataSet: SingleDataSet { public let id : UUID - public var dataPoints : [ChartDataPoint] + public var dataPoints : [BarChartDataPoint] public var legendTitle : String public var pointStyle : PointStyle public var style : BarStyle - public init(dataPoints : [ChartDataPoint], + public init(dataPoints : [BarChartDataPoint], legendTitle : String, pointStyle : PointStyle, style : BarStyle diff --git a/Sources/SwiftUICharts/BarChart/Views/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/Bars.swift index cc7cee0e..abd7de5b 100644 --- a/Sources/SwiftUICharts/BarChart/Views/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/Bars.swift @@ -10,13 +10,13 @@ import SwiftUI struct ColourBar: View { let colour : Color - let data : ChartDataPoint + let data : BarChartDataPoint let maxValue : Double let chartStyle : ChartStyle let style : BarStyle init(_ colour : Color, - _ data : ChartDataPoint, + _ data : BarChartDataPoint, _ maxValue : Double, _ chartStyle : ChartStyle, _ style : BarStyle @@ -46,7 +46,7 @@ struct GradientColoursBar: View { let colours : [Color] let startPoint : UnitPoint let endPoint : UnitPoint - let data : ChartDataPoint + let data : BarChartDataPoint let maxValue : Double let chartStyle : ChartStyle let style : BarStyle @@ -54,7 +54,7 @@ struct GradientColoursBar: View { init(_ colours : [Color], _ startPoint : UnitPoint, _ endPoint : UnitPoint, - _ data : ChartDataPoint, + _ data : BarChartDataPoint, _ maxValue : Double, _ chartStyle : ChartStyle, _ style : BarStyle @@ -88,7 +88,7 @@ struct GradientStopsBar: View { let stops : [Gradient.Stop] let startPoint : UnitPoint let endPoint : UnitPoint - let data : ChartDataPoint + let data : BarChartDataPoint let maxValue : Double let chartStyle : ChartStyle let style : BarStyle @@ -96,7 +96,7 @@ struct GradientStopsBar: View { init(_ stops : [Gradient.Stop], _ startPoint : UnitPoint, _ endPoint : UnitPoint, - _ data : ChartDataPoint, + _ data : BarChartDataPoint, _ maxValue : Double, _ chartStyle : ChartStyle, _ style : BarStyle diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index bf6cc811..333c9917 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -28,7 +28,7 @@ public class LineChartData: ChartData { @Published public var legends : [LegendData] /// Data model to hold data about the Views layout. - @Published public var viewData : ChartViewData + @Published public var viewData : ChartViewData public var noDataText : Text = Text("No Data") @@ -53,7 +53,7 @@ public class LineChartData: ChartData { metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, chartStyle : ChartStyle = ChartStyle(), - customCalc : @escaping ([ChartDataPoint]) -> [ChartDataPoint]? + customCalc : @escaping ([LineChartDataPoint]) -> [LineChartDataPoint]? ) { self.dataSets = dataSets self.metadata = metadata diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift new file mode 100644 index 00000000..e65fc893 --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift @@ -0,0 +1,40 @@ +// +// LineChartDataPoint.swift +// +// +// Created by Will Dale on 24/01/2021. +// + +import SwiftUI + +/// Data model for a data point. +public struct LineChartDataPoint: ChartDataPoint { + + 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? + + /// 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. + public init(value : Double, + xAxisLabel : String? = nil, + pointLabel : String? = nil, + date : Date? = nil + ) { + self.value = value + self.xAxisLabel = xAxisLabel + self.pointDescription = pointLabel + self.date = date + } +} diff --git a/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift index da4fe4d8..7b9a6f0d 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift @@ -10,12 +10,12 @@ import SwiftUI public struct LineDataSet: SingleDataSet { public let id : UUID - public var dataPoints : [ChartDataPoint] + public var dataPoints : [LineChartDataPoint] public var legendTitle : String public var pointStyle : PointStyle public var style : Styling - public init(dataPoints : [ChartDataPoint], + public init(dataPoints : [LineChartDataPoint], legendTitle : String, pointStyle : PointStyle = PointStyle(), style : LineDataSet.Styling diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift index 684dc437..1ad07477 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -28,7 +28,7 @@ public class MultiLineChartData: ChartData { @Published public var legends : [LegendData] /// Data model to hold data about the Views layout. - @Published public var viewData : ChartViewData + @Published public var viewData : ChartViewData public var noDataText : Text = Text("No Data") @@ -53,7 +53,7 @@ public class MultiLineChartData: ChartData { metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, chartStyle : ChartStyle = ChartStyle(), - customCalc : @escaping ([ChartDataPoint]) -> [ChartDataPoint]? + customCalc : @escaping ([LineChartDataPoint]) -> [LineChartDataPoint]? ) { self.dataSets = dataSets self.metadata = metadata diff --git a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift index da60cec5..7abcfbea 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift @@ -50,7 +50,7 @@ extension LineShape { func straightLine(_ rect : CGRect, _ x : CGFloat, _ y : CGFloat, - _ dataPoints : [ChartDataPoint], + _ dataPoints : [LineChartDataPoint], _ minValue : Double, _ range : Double, _ isFilled : Bool @@ -76,7 +76,7 @@ extension LineShape { func curvedLine(_ rect : CGRect, _ x : CGFloat, _ y : CGFloat, - _ dataPoints : [ChartDataPoint], + _ dataPoints : [LineChartDataPoint], _ minValue : Double, _ range : Double, _ isFilled : Bool @@ -107,7 +107,7 @@ extension LineShape { return path } - func filled(_ path: inout Path, _ rect: CGRect, _ x : CGFloat, _ y : CGFloat, _ dataPoints: [ChartDataPoint]) { + func filled(_ path: inout Path, _ rect: CGRect, _ x : CGFloat, _ y : CGFloat, _ dataPoints: [LineChartDataPoint]) { // Draw line straight down path.addLine(to: CGPoint(x: CGFloat(dataPoints.count-1) * x, y: rect.height)) diff --git a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift index 9adf7a19..799f5809 100644 --- a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift +++ b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift @@ -12,23 +12,23 @@ struct DataFunctions { // MARK: - Just DataPoints /// Get the highest value from dataPoints array. /// - Returns: Highest value. - static func maxValue(dataPoints: [ChartDataPoint]) -> Double { + static func maxValue(dataPoints: [D]) -> Double { return dataPoints.max { $0.value < $1.value }?.value ?? 0 } /// Get the Lowest value from dataPoints array. /// - Returns: Lowest value. - static func minValue(dataPoints: [ChartDataPoint]) -> Double { + static func minValue(dataPoints: [D]) -> Double { return dataPoints.min { $0.value < $1.value }?.value ?? 0 } /// Get the average of all the dataPoints. /// - Returns: Average. - static func average(dataPoints: [ChartDataPoint]) -> Double { + static func average(dataPoints: [D]) -> 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. - static func range(dataPoints: [ChartDataPoint]) -> Double { + static func range(dataPoints: [D]) -> Double { let maxValue = dataPoints.max { $0.value < $1.value }?.value ?? 0 let minValue = dataPoints.min { $0.value < $1.value }?.value ?? 0 diff --git a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift index e68d2655..763d855e 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift @@ -8,8 +8,8 @@ import Foundation /// Data model to pass view information internally so the layout can configure its self. -public struct ChartViewData { - +public struct ChartViewData { + /// Pass the type of chart being used to view modifiers. var chartType : ChartType = .line @@ -34,7 +34,7 @@ public struct ChartViewData { Used by TitleBox */ - var touchOverlayInfo : [ChartDataPoint] = [] + var touchOverlayInfo : [D] = [] /** Set specifier of data point readout diff --git a/Sources/SwiftUICharts/Shared/Models/LegendData.swift b/Sources/SwiftUICharts/Shared/Models/LegendData.swift index 50bf0b8a..20b29786 100644 --- a/Sources/SwiftUICharts/Shared/Models/LegendData.swift +++ b/Sources/SwiftUICharts/Shared/Models/LegendData.swift @@ -8,27 +8,27 @@ import SwiftUI /// Data model for Legends -public struct LegendData: Hashable { +public struct LegendData: CTColourStyle, Hashable { var chartType : ChartType - + public var colourType : ColourType /// Text to be displayed var legend : String - + /// Style of the stroke var strokeStyle : Stroke? /// Single Colour - var colour : Color? + public var colour : Color? /// Colours for Gradient - var colours : [Color]? + public var colours : [Color]? /// Colours and Stops for Gradient with stop control - var stops : [GradientStop]? + public var stops : [GradientStop]? /// Start point for Gradient - var startPoint : UnitPoint? + public var startPoint : UnitPoint? /// End point for Gradient - var endPoint : UnitPoint? + public var endPoint : UnitPoint? /// Used to make sure the charts data legend is first let prioity : Int @@ -40,10 +40,10 @@ public struct LegendData: Hashable { /// - strokeStyle: Stroke Style /// - prioity: Used to make sure the charts data legend is first public init(legend : String, - colour : Color, - strokeStyle: Stroke?, - prioity : Int, - chartType : ChartType + colour : Color, + strokeStyle: Stroke?, + prioity : Int, + chartType : ChartType ) { self.legend = legend self.colour = colour @@ -54,6 +54,7 @@ public struct LegendData: Hashable { self.strokeStyle = strokeStyle self.prioity = prioity self.chartType = chartType + self.colourType = .colour } /// Legend with a gradient colour @@ -65,12 +66,12 @@ public struct LegendData: Hashable { /// - strokeStyle: Stroke Style /// - prioity: Used to make sure the charts data legend is first public init(legend : String, - colours : [Color], - startPoint : UnitPoint, - endPoint : UnitPoint, - strokeStyle: Stroke?, - prioity : Int, - chartType : ChartType + colours : [Color], + startPoint : UnitPoint, + endPoint : UnitPoint, + strokeStyle: Stroke?, + prioity : Int, + chartType : ChartType ) { self.legend = legend self.colour = nil @@ -81,6 +82,7 @@ public struct LegendData: Hashable { self.strokeStyle = strokeStyle self.prioity = prioity self.chartType = chartType + self.colourType = .gradientColour } /// Legend with a gradient with stop control @@ -92,12 +94,12 @@ public struct LegendData: Hashable { /// - strokeStyle: Stroke Style /// - prioity: Used to make sure the charts data legend is first public init(legend : String, - stops : [GradientStop], - startPoint : UnitPoint, - endPoint : UnitPoint, - strokeStyle: Stroke?, - prioity : Int, - chartType : ChartType + stops : [GradientStop], + startPoint : UnitPoint, + endPoint : UnitPoint, + strokeStyle: Stroke?, + prioity : Int, + chartType : ChartType ) { self.legend = legend self.colour = nil @@ -108,5 +110,6 @@ public struct LegendData: Hashable { self.strokeStyle = strokeStyle self.prioity = prioity self.chartType = chartType + self.colourType = .gradientStops } } diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols.swift index bbd88801..a594b0b5 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols.swift @@ -9,15 +9,16 @@ import SwiftUI public protocol ChartData: ObservableObject, Identifiable { associatedtype Set : DataSet + associatedtype DataPoint : ChartDataPoint var id : UUID { get } var dataSets : Set { get set } var metadata : ChartMetadata? { get set } var xAxisLabels : [String]? { get set } var chartStyle : ChartStyle { get set } var legends : [LegendData] { get set } - var viewData : ChartViewData { get set } + var viewData : ChartViewData { get set } var noDataText : Text { get set } - var chartType : (ChartType, DataSetType) { get } + var chartType : (ChartType, DataSetType) { get } func legendOrder() -> [LegendData] } extension ChartData { @@ -33,7 +34,8 @@ public protocol DataSet: Hashable, Identifiable { public protocol SingleDataSet: DataSet { associatedtype Styling : CTColourStyle - var dataPoints : [ChartDataPoint] { get set } + associatedtype DataPoint : ChartDataPoint + var dataPoints : [DataPoint] { get set } var legendTitle : String { get set } var pointStyle : PointStyle { get set } var style : Styling { get set } @@ -56,11 +58,10 @@ public protocol CTColourStyle { public protocol ChartDataPoint: Hashable, Identifiable { var id : ID { get } - - var value : Double - var xAxisLabel : String? - var pointDescription : String? - var date : Date? + var value : Double { get set } + var xAxisLabel : String? { get set } + var pointDescription : String? { get set } + var date : Date? { get set } } diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index 1ad555ef..0f158180 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -7,15 +7,15 @@ import SwiftUI -internal struct TouchOverlayBox: View { +internal struct TouchOverlayBox: View { - private var selectedPoints : [ChartDataPoint] + private var selectedPoints : [D] private var specifier : String private var ignoreZero : Bool @Binding private var boxFrame : CGRect - internal init(selectedPoints : [ChartDataPoint], + internal init(selectedPoints : [D], specifier : String = "%.0f", boxFrame : Binding, ignoreZero : Bool = false From dd668e8403b138f8a31fb5725ccb4217e83242a3 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 27 Jan 2021 10:47:14 +0000 Subject: [PATCH 007/152] Tidy Up and convert View Modifiers to new protocols. --- .../BarChart/Models/BarChartData.swift | 65 +---- .../BarChart/Models/BarChartStyle.swift | 63 ++++ .../BarChart/Models/BarDataSet.swift | 4 +- .../BarChart/Models/MultiBarChartData.swift | 64 +++++ .../BarChart/Views/BarChartView.swift | 122 ++------ .../BarChart/Views/GroupedBarChart.swift | 60 ++++ .../SubViews/BarChartDataSetSubView.swift | 82 ++++++ .../BarChart/Views/{ => SubViews}/Bars.swift | 12 +- .../LineChart/Models/LineChartData.swift | 28 +- .../LineChart/Models/LineChartStyle.swift | 64 +++++ .../LineChart/Models/LineDataSet.swift | 3 +- .../LineChart/Models/MultiLineChartData.swift | 27 +- .../LineChart/Views/FilledLineChart.swift | 57 ++-- .../LineChart/Views/LineChartView.swift | 152 ++++------ .../LineChart/Views/MultiLineChart.swift | 63 ++-- .../Views/SubViews/LineChartSubViews.swift | 129 +++++++++ .../PieChart/Models/PieChartData.swift | 116 ++++++++ .../Models/PieChartStyle.swift} | 8 +- .../PieChart/Shapes/PieSegmentShape.swift | 39 +++ .../PieChart/Views/PieChart.swift | 61 ++++ .../Shared/Extras/AddLegends.swift | 83 ++++++ .../Shared/Extras/DataFunctions.swift | 31 +- .../SwiftUICharts/Shared/Extras/Enums.swift | 2 + .../Shared/Extras/Extensions.swift | 14 + .../Shared/Models/ChartViewData.swift | 2 +- .../Shared/Models/LegendData.swift | 4 +- .../Shared/Models/Protocols.swift | 56 +++- .../SwiftUICharts/Shared/Shapes/Marker.swift | 2 + .../Shared/ViewModifiers/AxisBorders.swift | 8 +- .../Shared/ViewModifiers/HeaderBox.swift | 195 ++++++------- .../Shared/ViewModifiers/Legends.swift | 36 +-- .../Shared/ViewModifiers/XAxisGrid.swift | 6 +- .../Shared/ViewModifiers/XAxisLabels.swift | 221 +++++++++------ .../Shared/ViewModifiers/YAxisGrid.swift | 6 +- .../Shared/ViewModifiers/YAxisLabels.swift | 268 ++++++++++-------- .../Shared/Views/LegendView.swift | 2 + 36 files changed, 1424 insertions(+), 731 deletions(-) create mode 100644 Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift create mode 100644 Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift create mode 100644 Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift create mode 100644 Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartDataSetSubView.swift rename Sources/SwiftUICharts/BarChart/Views/{ => SubViews}/Bars.swift (94%) create mode 100644 Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift create mode 100644 Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift create mode 100644 Sources/SwiftUICharts/PieChart/Models/PieChartData.swift rename Sources/SwiftUICharts/{Shared/Models/ChartStyle.swift => PieChart/Models/PieChartStyle.swift} (96%) create mode 100644 Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift create mode 100644 Sources/SwiftUICharts/PieChart/Views/PieChart.swift create mode 100644 Sources/SwiftUICharts/Shared/Extras/AddLegends.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index 63502c36..705f5b7a 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -7,23 +7,23 @@ import SwiftUI -public class BarChartData: ChartData { +public class BarChartData: LineAndBarChartData { public let id : UUID = UUID() @Published public var dataSets : BarDataSet @Published public var metadata : ChartMetadata? @Published public var xAxisLabels : [String]? - @Published public var chartStyle : ChartStyle + @Published public var chartStyle : BarChartStyle @Published public var legends : [LegendData] @Published public var viewData : ChartViewData public var noDataText : Text = Text("No Data") - public var chartType: (ChartType, DataSetType) + public var chartType : (chartType: ChartType, dataSetType: DataSetType) public init(dataSets : BarDataSet, metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, - chartStyle : ChartStyle = ChartStyle(), + chartStyle : BarChartStyle = BarChartStyle(), calculations: CalculationType = .none ) { self.dataSets = dataSets @@ -38,7 +38,7 @@ public class BarChartData: ChartData { public init(dataSets : BarDataSet, metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, - chartStyle : ChartStyle = ChartStyle(), + chartStyle : BarChartStyle = BarChartStyle(), customCalc : @escaping ([BarChartDataPoint]) -> [BarChartDataPoint]? ) { self.dataSets = dataSets @@ -47,59 +47,18 @@ public class BarChartData: ChartData { self.chartStyle = chartStyle self.legends = [LegendData]() self.viewData = ChartViewData() - self.chartType = (.bar, .single) - } - - public func legendOrder() -> [LegendData] { - return [LegendData]() - } - -} - -public class MultiBarChartData: ChartData { - - public let id : UUID = UUID() - - @Published public var dataSets : MultiBarDataSet - @Published public var metadata : ChartMetadata? - @Published public var xAxisLabels : [String]? - @Published public var chartStyle : ChartStyle - @Published public var legends : [LegendData] - @Published public var viewData : ChartViewData - public var noDataText : Text = Text("No Data") - public var chartType : (ChartType, DataSetType) - - public init(dataSets : MultiBarDataSet, - metadata : ChartMetadata? = nil, - xAxisLabels : [String]? = nil, - chartStyle : ChartStyle = ChartStyle(), - calculations: CalculationType = .none - ) { - self.dataSets = dataSets - self.metadata = metadata - self.xAxisLabels = xAxisLabels - self.chartStyle = chartStyle - self.legends = [LegendData]() - self.viewData = ChartViewData() - self.chartType = (.bar, .single) + self.chartType = (chartType: .bar, dataSetType: .single) } - public init(dataSets : MultiBarDataSet, - metadata : ChartMetadata? = nil, - xAxisLabels : [String]? = nil, - chartStyle : ChartStyle = ChartStyle(), - customCalc : @escaping ([BarChartDataPoint]) -> [BarChartDataPoint]? - ) { - self.dataSets = dataSets - self.metadata = metadata - self.xAxisLabels = xAxisLabels - self.chartStyle = chartStyle - self.legends = [LegendData]() - self.viewData = ChartViewData() - self.chartType = (.bar, .single) + public func getHeaderLocation() -> InfoBoxPlacement { + return self.chartStyle.infoBoxPlacement } public func legendOrder() -> [LegendData] { return [LegendData]() } + + public typealias Set = BarDataSet + public typealias DataPoint = BarChartDataPoint } + diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift new file mode 100644 index 00000000..3fdbeeb3 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift @@ -0,0 +1,63 @@ +// +// BarChartStyle.swift +// +// +// Created by Will Dale on 25/01/2021. +// + +import SwiftUI + +public struct BarChartStyle: CTLineAndBarChartStyle { + + /// 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/BarChart/Models/BarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift index b2cd8469..8f2e8e4a 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift @@ -35,9 +35,9 @@ public struct MultiBarDataSet: MultiDataSet { public let id : UUID - public var dataSets : [LineDataSet] + public var dataSets : [BarDataSet] - public init(dataSets: [LineDataSet]) { + public init(dataSets: [BarDataSet]) { self.id = UUID() self.dataSets = dataSets } diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift new file mode 100644 index 00000000..a8720ae2 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift @@ -0,0 +1,64 @@ +// +// MultiBarChartData.swift +// +// +// Created by Will Dale on 26/01/2021. +// + +import SwiftUI + +public class MultiBarChartData: LineAndBarChartData { + + public let id : UUID = UUID() + + @Published public var dataSets : MultiBarDataSet + @Published public var metadata : ChartMetadata? + @Published public var xAxisLabels : [String]? + @Published public var chartStyle : BarChartStyle + @Published public var legends : [LegendData] + @Published public var viewData : ChartViewData + public var noDataText : Text = Text("No Data") + public var chartType : (chartType: ChartType, dataSetType: DataSetType) + + public init(dataSets : MultiBarDataSet, + metadata : ChartMetadata? = nil, + xAxisLabels : [String]? = nil, + chartStyle : BarChartStyle = BarChartStyle(), + calculations: CalculationType = .none + ) { + self.dataSets = dataSets + self.metadata = metadata + self.xAxisLabels = xAxisLabels + self.chartStyle = chartStyle + self.legends = [LegendData]() + self.viewData = ChartViewData() + self.chartType = (chartType: .bar, dataSetType: .multi) + } + + public init(dataSets : MultiBarDataSet, + metadata : ChartMetadata? = nil, + xAxisLabels : [String]? = nil, + chartStyle : BarChartStyle = BarChartStyle(), + customCalc : @escaping ([BarChartDataPoint]) -> [BarChartDataPoint]? + ) { + self.dataSets = dataSets + self.metadata = metadata + self.xAxisLabels = xAxisLabels + self.chartStyle = chartStyle + self.legends = [LegendData]() + self.viewData = ChartViewData() + self.chartType = (chartType: .bar, dataSetType: .multi) + } + + public func legendOrder() -> [LegendData] { + return [LegendData]() + } + + public func getHeaderLocation() -> InfoBoxPlacement { + return self.chartStyle.infoBoxPlacement + } + + public typealias Set = MultiBarDataSet + public typealias DataPoint = BarChartDataPoint +} + diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift b/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift index 06b1f6c0..949d987d 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift @@ -16,110 +16,38 @@ public struct BarChart: View where ChartData: BarChartData { public init(chartData: ChartData) { self.chartData = chartData self.maxValue = DataFunctions.maxValue(dataPoints: chartData.dataSets.dataPoints) - chartData.viewData.chartType = .bar +// chartData.viewData.chartType = .bar + + setupLegends() } public var body: some View { HStack(spacing: 0) { ForEach(chartData.dataSets.dataPoints) { dataPoint in - ColourBar(chartData.dataSets.style.colour!, dataPoint, maxValue, chartData.chartStyle, chartData.dataSets.style) + + switch chartData.dataSets.style.colourFrom { + case .barStyle: + + BarChartDataSetSubView(colourType: chartData.dataSets.style.colourType, + dataPoint: dataPoint, + style: chartData.dataSets.style, + chartStyle: chartData.chartStyle, + maxValue: maxValue) + + case .dataPoints: + + BarChartDataPointSubView(colourType : dataPoint.colourType, + dataPoint : dataPoint, + style : chartData.dataSets.style, + chartStyle : chartData.chartStyle, + maxValue : maxValue) + + } } } } + internal mutating func setupLegends() { + AddLegends.setupBar(chartData: &chartData, dataSet: chartData.dataSets) + } } - - - -// 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/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift new file mode 100644 index 00000000..9a5d6897 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -0,0 +1,60 @@ +// +// GroupedBarChart.swift +// +// +// Created by Will Dale on 25/01/2021. +// + +import SwiftUI + +public struct GroupedBarChart: View where ChartData: MultiBarChartData { + + @ObservedObject var chartData: ChartData + + let maxValue : Double + + public init(chartData: ChartData) { + self.chartData = chartData + self.maxValue = DataFunctions.multiDataSetMaxValue(from: chartData.dataSets) +// chartData.viewData.chartType = .bar + +// setupLegends() + } + + public var body: some View { + HStack(spacing: 100) { + ForEach(chartData.dataSets.dataSets) { dataSet in + VStack { + HStack(spacing: 0) { + ForEach(dataSet.dataPoints) { dataPoint in + + switch dataSet.style.colourFrom { + case .barStyle: + + BarChartDataSetSubView(colourType: dataSet.style.colourType, + dataPoint: dataPoint, + style: dataSet.style, + chartStyle: chartData.chartStyle, + maxValue: maxValue) + + case .dataPoints: + + BarChartDataPointSubView(colourType: dataPoint.colourType, + dataPoint: dataPoint, + style: dataSet.style, + chartStyle: chartData.chartStyle, + maxValue: maxValue) + + } + } + } + Text(dataSet.legendTitle) + + } + } + } + } +// internal mutating func setupLegends() { +// Legends.setupBar(chartData: &chartData, dataSet: chartData.dataSets) +// } +} diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartDataSetSubView.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartDataSetSubView.swift new file mode 100644 index 00000000..bd9edede --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartDataSetSubView.swift @@ -0,0 +1,82 @@ +// +// BarChartDataSetSubView.swift +// +// +// Created by Will Dale on 26/01/2021. +// + +import SwiftUI + +internal struct BarChartDataSetSubView: View { + + let colourType : ColourType + let dataPoint : BarChartDataPoint + let style : BarStyle + let chartStyle : BarChartStyle + let maxValue : Double + + internal var body: some View { + if colourType == .colour, + let colour = style.colour + { + ColourBar(colour, dataPoint, maxValue, chartStyle, style) + + } else if colourType == .gradientColour, + let colours = style.colours, + let startPoint = style.startPoint, + let endPoint = style.endPoint + { + + GradientColoursBar(colours, startPoint, endPoint, dataPoint, maxValue, chartStyle, style) + + } else if colourType == .gradientStops, + let stops = style.stops, + let startPoint = style.startPoint, + let endPoint = style.endPoint + { + + let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) + GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, maxValue, chartStyle, style) + + } + } +} + + +internal struct BarChartDataPointSubView: View { + + let colourType : ColourType + let dataPoint : BarChartDataPoint + let style : BarStyle + let chartStyle : BarChartStyle + let maxValue : Double + + internal var body: some View { + + if dataPoint.colourType == .colour, + let colour = dataPoint.colour + { + + ColourBar(colour, dataPoint, maxValue, chartStyle, style) + + } else if dataPoint.colourType == .gradientColour, + let colours = dataPoint.colours, + let startPoint = dataPoint.startPoint, + let endPoint = dataPoint.endPoint + { + + GradientColoursBar(colours, startPoint, endPoint, dataPoint, maxValue, chartStyle, style) + + } else if dataPoint.colourType == .gradientStops, + let stops = dataPoint.stops, + let startPoint = dataPoint.startPoint, + let endPoint = dataPoint.endPoint + { + + let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) + + GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, maxValue, chartStyle, style) + } + + } +} diff --git a/Sources/SwiftUICharts/BarChart/Views/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift similarity index 94% rename from Sources/SwiftUICharts/BarChart/Views/Bars.swift rename to Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift index abd7de5b..de9075d6 100644 --- a/Sources/SwiftUICharts/BarChart/Views/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift @@ -12,13 +12,13 @@ struct ColourBar: View { let colour : Color let data : BarChartDataPoint let maxValue : Double - let chartStyle : ChartStyle + let chartStyle : BarChartStyle let style : BarStyle init(_ colour : Color, _ data : BarChartDataPoint, _ maxValue : Double, - _ chartStyle : ChartStyle, + _ chartStyle : BarChartStyle, _ style : BarStyle ) { self.colour = colour @@ -48,7 +48,7 @@ struct GradientColoursBar: View { let endPoint : UnitPoint let data : BarChartDataPoint let maxValue : Double - let chartStyle : ChartStyle + let chartStyle : BarChartStyle let style : BarStyle init(_ colours : [Color], @@ -56,7 +56,7 @@ struct GradientColoursBar: View { _ endPoint : UnitPoint, _ data : BarChartDataPoint, _ maxValue : Double, - _ chartStyle : ChartStyle, + _ chartStyle : BarChartStyle, _ style : BarStyle ) { self.colours = colours @@ -90,7 +90,7 @@ struct GradientStopsBar: View { let endPoint : UnitPoint let data : BarChartDataPoint let maxValue : Double - let chartStyle : ChartStyle + let chartStyle : BarChartStyle let style : BarStyle init(_ stops : [Gradient.Stop], @@ -98,7 +98,7 @@ struct GradientStopsBar: View { _ endPoint : UnitPoint, _ data : BarChartDataPoint, _ maxValue : Double, - _ chartStyle : ChartStyle, + _ chartStyle : BarChartStyle, _ style : BarStyle ) { self.stops = stops diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index 333c9917..c0137c19 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -8,12 +8,12 @@ import SwiftUI /// The central model from which the chart is drawn. -public class LineChartData: ChartData { +public class LineChartData: LineAndBarChartData { public let id : UUID = UUID() /// Data model containing the datapoints: Value, Label, Description and Date. Individual colouring for bar chart. - @Published public var dataSets : Set + @Published public var dataSets : LineDataSet /// Data model containing: the charts Title, the charts Subtitle and the Line Legend. @Published public var metadata : ChartMetadata? @@ -22,7 +22,7 @@ public class LineChartData: ChartData { @Published public var xAxisLabels : [String]? /// Data model conatining the style data for the chart. - @Published public var chartStyle : ChartStyle + @Published public var chartStyle : LineChartStyle /// Array of data to populate the chart legend. @Published public var legends : [LegendData] @@ -32,12 +32,12 @@ public class LineChartData: ChartData { public var noDataText : Text = Text("No Data") - public var chartType : (ChartType, DataSetType) + public var chartType : (chartType: ChartType, dataSetType: DataSetType) - public init(dataSets : Set, + public init(dataSets : LineDataSet, metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, - chartStyle : ChartStyle = ChartStyle(), + chartStyle : LineChartStyle = LineChartStyle(), calculations: CalculationType = .none ) { self.dataSets = dataSets @@ -46,13 +46,13 @@ public class LineChartData: ChartData { self.chartStyle = chartStyle self.legends = [LegendData]() self.viewData = ChartViewData() - self.chartType = (.line, .single) + self.chartType = (chartType: .line, dataSetType: .single) } - public init(dataSets : Set, + public init(dataSets : LineDataSet, metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, - chartStyle : ChartStyle = ChartStyle(), + chartStyle : LineChartStyle = LineChartStyle(), customCalc : @escaping ([LineChartDataPoint]) -> [LineChartDataPoint]? ) { self.dataSets = dataSets @@ -61,9 +61,13 @@ public class LineChartData: ChartData { self.chartStyle = chartStyle self.legends = [LegendData]() self.viewData = ChartViewData() - self.chartType = (.line, .single) + self.chartType = (chartType: .line, dataSetType: .single) } - - public typealias Set = LineDataSet + public func getHeaderLocation() -> InfoBoxPlacement { + return self.chartStyle.infoBoxPlacement + } + + public typealias Set = LineDataSet + public typealias DataPoint = LineChartDataPoint } diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift new file mode 100644 index 00000000..33a5f764 --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift @@ -0,0 +1,64 @@ +// +// LineChartStyle.swift +// +// +// Created by Will Dale on 25/01/2021. +// + +import SwiftUI + +/// Model for controlling the overall aesthetic of the chart. +public struct LineChartStyle: CTLineAndBarChartStyle { + + /// 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/LineChart/Models/LineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift index 7b9a6f0d..dee71eeb 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift @@ -18,7 +18,7 @@ public struct LineDataSet: SingleDataSet { public init(dataPoints : [LineChartDataPoint], legendTitle : String, pointStyle : PointStyle = PointStyle(), - style : LineDataSet.Styling + style : LineStyle = LineStyle() ) { self.id = UUID() self.dataPoints = dataPoints @@ -32,7 +32,6 @@ public struct LineDataSet: SingleDataSet { } - public struct MultiLineDataSet: MultiDataSet { public let id : UUID diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift index 1ad07477..2cbd87bd 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -8,12 +8,12 @@ import SwiftUI /// The central model from which the chart is drawn. -public class MultiLineChartData: ChartData { +public class MultiLineChartData: LineAndBarChartData { public let id : UUID = UUID() /// Data model containing the datapoints: Value, Label, Description and Date. Individual colouring for bar chart. - @Published public var dataSets : Set + @Published public var dataSets : MultiLineDataSet /// Data model containing: the charts Title, the charts Subtitle and the Line Legend. @Published public var metadata : ChartMetadata? @@ -22,7 +22,7 @@ public class MultiLineChartData: ChartData { @Published public var xAxisLabels : [String]? /// Data model conatining the style data for the chart. - @Published public var chartStyle : ChartStyle + @Published public var chartStyle : LineChartStyle /// Array of data to populate the chart legend. @Published public var legends : [LegendData] @@ -32,12 +32,12 @@ public class MultiLineChartData: ChartData { public var noDataText : Text = Text("No Data") - public var chartType : (ChartType, DataSetType) + public var chartType : (chartType: ChartType, dataSetType: DataSetType) - public init(dataSets : Set, + public init(dataSets : MultiLineDataSet, metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, - chartStyle : ChartStyle = ChartStyle(), + chartStyle : LineChartStyle = LineChartStyle(), calculations: CalculationType = .none ) { self.dataSets = dataSets @@ -49,10 +49,10 @@ public class MultiLineChartData: ChartData { self.chartType = (.line, .multi) } - public init(dataSets : Set, + public init(dataSets : MultiLineDataSet, metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, - chartStyle : ChartStyle = ChartStyle(), + chartStyle : LineChartStyle = LineChartStyle(), customCalc : @escaping ([LineChartDataPoint]) -> [LineChartDataPoint]? ) { self.dataSets = dataSets @@ -61,8 +61,17 @@ public class MultiLineChartData: ChartData { self.chartStyle = chartStyle self.legends = [LegendData]() self.viewData = ChartViewData() - self.chartType = (.line, .multi) + self.chartType = (chartType: .line, dataSetType: .multi) + } + + public func getHeaderLocation() -> InfoBoxPlacement { + return self.chartStyle.infoBoxPlacement + } + + public func getDataSet() -> MultiLineDataSet { + return self.dataSets } public typealias Set = MultiLineDataSet + public typealias DataPoint = LineChartDataPoint } diff --git a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift index 1e4c8505..88236e11 100644 --- a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift @@ -23,57 +23,36 @@ public struct FilledLineChart: View where ChartData: LineChartData { } public var body: some View { - - let style : LineStyle = chartData.dataSets.style - + // if chartData.isGreaterThanTwo { - if style.colourType == .colour, - let colour = style.colour + if chartData.dataSets.style.colourType == .colour, + let colour = chartData.dataSets.style.colour { - LineShape(dataSet: chartData.dataSets, lineType: style.lineType, isFilled: true, minValue: minValue, range: range) - .scale(y: startAnimation ? 1 : 0, anchor: .bottom) - .fill(colour) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - } else if style.colourType == .gradientColour, - let colours = style.colours, - let startPoint = style.startPoint, - let endPoint = style.endPoint + + LineChartColourSubView(chartData: chartData, dataSet: chartData.dataSets, style: chartData.dataSets.style, minValue: minValue, range: range, colour: colour, isFilled: true) + + } else if chartData.dataSets.style.colourType == .gradientColour, + let colours = chartData.dataSets.style.colours, + let startPoint = chartData.dataSets.style.startPoint, + let endPoint = chartData.dataSets.style.endPoint { - LineShape(dataSet: chartData.dataSets, lineType: style.lineType, isFilled: true, minValue: minValue, range: range) - .scale(y: startAnimation ? 1 : 0, anchor: .bottom) - .fill(LinearGradient(gradient: Gradient(colors: colours), - startPoint: startPoint, - endPoint: endPoint)) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } + LineChartColoursSubView(chartData: chartData, dataSet: chartData.dataSets, style: chartData.dataSets.style, minValue: minValue, range: range, colours: colours, startPoint: startPoint, endPoint: endPoint, isFilled: true) - } else if style.colourType == .gradientStops, - let stops = style.stops, - let startPoint = style.startPoint, - let endPoint = style.endPoint + } else if chartData.dataSets.style.colourType == .gradientStops, + let stops = chartData.dataSets.style.stops, + let startPoint = chartData.dataSets.style.startPoint, + let endPoint = chartData.dataSets.style.endPoint { let stops = GradientStop.convertToGradientStopsArray(stops: stops) - LineShape(dataSet: chartData.dataSets, lineType: style.lineType, isFilled: true, minValue: minValue, range: range) - .scale(y: startAnimation ? 1 : 0, anchor: .bottom) - .fill(LinearGradient(gradient: Gradient(stops: stops), - startPoint: startPoint, - endPoint: endPoint)) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } + LineChartStopsSubView(chartData: chartData, dataSet: chartData.dataSets, style: chartData.dataSets.style, minValue: minValue, range: range, stops: stops, startPoint: startPoint, endPoint: endPoint, isFilled: true) + } // } else { CustomNoDataView(chartData: chartData) } } internal mutating func setupLegends() { - LineLegends.setup(chartData: &chartData, dataSet: chartData.dataSets) + AddLegends.setupLine(chartData: &chartData, dataSet: chartData.dataSets) } } diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index d4d65c68..6fb279e9 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -26,115 +26,73 @@ public struct LineChart: View where ChartData: LineChartData { public var body: some View { - let style : LineStyle = chartData.dataSets.style - let strokeStyle = style.strokeStyle - // if chartData.isGreaterThanTwo { - if style.colourType == .colour, - let colour = style.colour - { - LineShape(dataSet: chartData.dataSets, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) - .trim(to: startAnimation ? 1 : 0) - .stroke(colour, style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - } else if style.colourType == .gradientColour, - let colours = style.colours, - let startPoint = style.startPoint, - let endPoint = style.endPoint + if chartData.dataSets.style.colourType == .colour, + let colour = chartData.dataSets.style.colour { + LineChartColourSubView(chartData: chartData, + dataSet: chartData.dataSets, + style: chartData.dataSets.style, + minValue: minValue, + range: range, + colour: colour, + isFilled: false) - LineShape(dataSet: chartData.dataSets, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) - .trim(to: startAnimation ? 1 : 0) - .stroke(LinearGradient(gradient: Gradient(colors: colours), - startPoint: startPoint, - endPoint: endPoint), - style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - - } else if style.colourType == .gradientStops, - let stops = style.stops, - let startPoint = style.startPoint, - let endPoint = style.endPoint + } else if chartData.dataSets.style.colourType == .gradientColour, + let colours = chartData.dataSets.style.colours, + let startPoint = chartData.dataSets.style.startPoint, + let endPoint = chartData.dataSets.style.endPoint + { + + LineChartColoursSubView(chartData: chartData, + dataSet: chartData.dataSets, + style: chartData.dataSets.style, + minValue: minValue, + range: range, + colours: colours, + startPoint: startPoint, + endPoint: endPoint, + isFilled: false) + + } else if chartData.dataSets.style.colourType == .gradientStops, + let stops = chartData.dataSets.style.stops, + let startPoint = chartData.dataSets.style.startPoint, + let endPoint = chartData.dataSets.style.endPoint { let stops = GradientStop.convertToGradientStopsArray(stops: stops) + + LineChartStopsSubView(chartData: chartData, + dataSet: chartData.dataSets, + style: chartData.dataSets.style, + minValue: minValue, + range: range, + stops: stops, + startPoint: startPoint, + endPoint: endPoint, + isFilled: false) - LineShape(dataSet: chartData.dataSets, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) - .trim(to: startAnimation ? 1 : 0) - .stroke(LinearGradient(gradient: Gradient(stops: stops), - startPoint: startPoint, - endPoint: endPoint), - style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } } // } else { CustomNoDataView(chartData: chartData) } } internal mutating func setupLegends() { - LineLegends.setup(chartData: &chartData, dataSet: chartData.dataSets) + AddLegends.setupLine(chartData: &chartData, dataSet: chartData.dataSets) } } - - - -internal struct LineShapeModifiers: ViewModifier { - private let chartData : T - - internal init(_ chartData : T) { - self.chartData = chartData - } - - func body(content: Content) -> some View { - content - .background(Color(.gray).opacity(0.01)) - .if(chartData.viewData.hasXAxisLabels) { $0.xAxisBorder(chartData: chartData) } - .if(chartData.viewData.hasYAxisLabels) { $0.yAxisBorder(chartData: chartData) } - } -} - -struct LineLegends { - static func setup(chartData: inout T, dataSet: LineDataSet) { - if dataSet.style.colourType == .colour, - let colour = dataSet.style.colour - { - chartData.legends.append(LegendData(legend : dataSet.legendTitle, - colour : colour, - strokeStyle: dataSet.style.strokeStyle, - prioity : 1, - chartType : .line)) - - } else if dataSet.style.colourType == .gradientColour, - let colours = dataSet.style.colours - { - chartData.legends.append(LegendData(legend : dataSet.legendTitle, - colours : colours, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: dataSet.style.strokeStyle, - prioity : 1, - chartType : .line)) - - } else if dataSet.style.colourType == .gradientStops, - let stops = dataSet.style.stops - { - chartData.legends.append(LegendData(legend : dataSet.legendTitle, - stops : stops, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: dataSet.style.strokeStyle, - prioity : 1, - chartType : .line)) - } - chartData.viewData.chartType = .line - } -} +//internal struct LineShapeModifiers: ViewModifier { +// private let chartData : T +// +// internal init(_ chartData : T) { +// self.chartData = chartData +// } +// +// func body(content: Content) -> some View { +// content +// .background(Color(.gray).opacity(0.01)) +// .if(chartData.viewData.hasXAxisLabels) { $0.xAxisBorder(chartData: chartData) } +// .if(chartData.viewData.hasYAxisLabels) { $0.yAxisBorder(chartData: chartData) } +// } +//} +// diff --git a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift index 3ff82eb1..963ed30f 100644 --- a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift @@ -29,56 +29,31 @@ public struct MultiLineChart: View where ChartData: MultiLineChartDat ZStack { ForEach(chartData.dataSets.dataSets, id: \.self) { dataSet in - let style : LineStyle = dataSet.style - let strokeStyle = style.strokeStyle - // if chartData.isGreaterThanTwo { - if style.colourType == .colour, - let colour = style.colour - { - LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) - .trim(to: startAnimation ? 1 : 0) - .stroke(colour, style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - - } else if style.colourType == .gradientColour, - let colours = style.colours, - let startPoint = style.startPoint, - let endPoint = style.endPoint + if dataSet.style.colourType == .colour, + let colour = dataSet.style.colour { + + LineChartColourSubView(chartData: chartData, dataSet: dataSet, style: dataSet.style, minValue: minValue, range: range, colour: colour, isFilled: false) - LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) - .trim(to: startAnimation ? 1 : 0) - .stroke(LinearGradient(gradient: Gradient(colors: colours), - startPoint: startPoint, - endPoint: endPoint), - style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } + } else if dataSet.style.colourType == .gradientColour, + let colours = dataSet.style.colours, + let startPoint = dataSet.style.startPoint, + let endPoint = dataSet.style.endPoint + { - } else if style.colourType == .gradientStops, - let stops = style.stops, - let startPoint = style.startPoint, - let endPoint = style.endPoint + LineChartColoursSubView(chartData: chartData, dataSet: dataSet, style: dataSet.style, minValue: minValue, range: range, colours: colours, startPoint: startPoint, endPoint: endPoint, isFilled: false) + + } else if dataSet.style.colourType == .gradientStops, + let stops = dataSet.style.stops, + let startPoint = dataSet.style.startPoint, + let endPoint = dataSet.style.endPoint { let stops = GradientStop.convertToGradientStopsArray(stops: stops) - - LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: false, minValue: minValue, range: range) - .trim(to: startAnimation ? 1 : 0) - .stroke(LinearGradient(gradient: Gradient(stops: stops), - startPoint: startPoint, - endPoint: endPoint), - style: Stroke.strokeToStrokeStyle(stroke: strokeStyle)) - .modifier(LineShapeModifiers(chartData)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } + + LineChartStopsSubView(chartData: chartData, dataSet: dataSet, style: dataSet.style, minValue: minValue, range: range, stops: stops, startPoint: startPoint, endPoint: endPoint, isFilled: false) + } } } @@ -86,7 +61,7 @@ public struct MultiLineChart: View where ChartData: MultiLineChartDat } internal mutating func setupLegends() { for dataSet in chartData.dataSets.dataSets { - LineLegends.setup(chartData: &chartData, dataSet: dataSet) + AddLegends.setupLine(chartData: &chartData, dataSet: dataSet) } } } diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift new file mode 100644 index 00000000..93b0f330 --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift @@ -0,0 +1,129 @@ +// +// LineChartSubViews.swift +// +// +// Created by Will Dale on 26/01/2021. +// + +import SwiftUI + +struct LineChartColourSubView: View where CD: LineAndBarChartData { + + let chartData : CD + let dataSet : LineDataSet + let style : LineStyle + let minValue : Double + let range : Double + let colour : Color + + let isFilled : Bool + + @State var startAnimation : Bool = false + + var body: some View { + + LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: isFilled, minValue: minValue, range: range) + .ifElse(isFilled, if: { + $0.scale(y: startAnimation ? 1 : 0, anchor: .bottom) + .fill(colour) + }, else: { + $0.trim(to: startAnimation ? 1 : 0) + .stroke(colour, style: Stroke.strokeToStrokeStyle(stroke: style.strokeStyle)) + }) + + .background(Color(.gray).opacity(0.01)) + .if(chartData.viewData.hasXAxisLabels) { $0.xAxisBorder(chartData: chartData) } + .if(chartData.viewData.hasYAxisLabels) { $0.yAxisBorder(chartData: chartData) } + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + } +} + +struct LineChartColoursSubView: View where CD: LineAndBarChartData { + + let chartData : CD + let dataSet : LineDataSet + let style : LineStyle + let minValue : Double + let range : Double + let colours : [Color] + let startPoint : UnitPoint + let endPoint : UnitPoint + + let isFilled : Bool + + @State var startAnimation : Bool = false + + var body: some View { + + LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: isFilled, minValue: minValue, range: range) + + .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: Stroke.strokeToStrokeStyle(stroke: style.strokeStyle)) + }) + + + .background(Color(.gray).opacity(0.01)) + .if(chartData.viewData.hasXAxisLabels) { $0.xAxisBorder(chartData: chartData) } + .if(chartData.viewData.hasYAxisLabels) { $0.yAxisBorder(chartData: chartData) } + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + } +} + +struct LineChartStopsSubView: View where CD: LineAndBarChartData { + + let chartData : CD + let dataSet : LineDataSet + let style : LineStyle + let minValue : Double + let range : Double + let stops : [Gradient.Stop] + let startPoint : UnitPoint + let endPoint : UnitPoint + + let isFilled : Bool + + @State var startAnimation : Bool = false + + var body: some View { + + LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: isFilled, minValue: minValue, range: range) + + .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: Stroke.strokeToStrokeStyle(stroke: style.strokeStyle)) + }) + + .background(Color(.gray).opacity(0.01)) + .if(chartData.viewData.hasXAxisLabels) { $0.xAxisBorder(chartData: chartData) } + .if(chartData.viewData.hasYAxisLabels) { $0.yAxisBorder(chartData: chartData) } + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + } +} + diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift new file mode 100644 index 00000000..92d68c1d --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift @@ -0,0 +1,116 @@ +// +// PieChartData.swift +// +// +// Created by Will Dale on 24/01/2021. +// + +import SwiftUI + +public class PieChartData: PieChartDataProtocol { + + @Published public var id: UUID = UUID() + @Published public var dataSets: PieDataSet + @Published public var metadata: ChartMetadata? + @Published public var xAxisLabels: [String]? + @Published public var chartStyle: PieChartStyle + @Published public var legends: [LegendData] + @Published public var viewData: ChartViewData + + public var noDataText: Text + public var chartType: (chartType: ChartType, dataSetType: DataSetType) + + public init(dataSets : PieDataSet, + metadata : ChartMetadata? = nil, + xAxisLabels : [String]? = nil, + chartStyle : PieChartStyle = PieChartStyle(), + noDataText : Text + ) { + self.dataSets = dataSets + self.metadata = metadata + self.xAxisLabels = xAxisLabels + self.chartStyle = chartStyle + self.legends = [LegendData]() + self.viewData = ChartViewData() + self.noDataText = noDataText + self.chartType = (chartType: .pie, dataSetType: .single) + } + + public func getHeaderLocation() -> InfoBoxPlacement { + return self.chartStyle.infoBoxPlacement + } + + public typealias Set = PieDataSet + public typealias DataPoint = PieChartDataPoint +} + +public struct PieChartDataPoint: ChartDataPoint { + + public var id : UUID = UUID() + public var value : Double + public var xAxisLabel : String? + public var pointDescription : String? + public var date : Date? + + public var colour : Color + + public init(value : Double, + xAxisLabel : String? = nil, + pointDescription: String? = nil, + date : Date? = nil, + colour : Color = Color.red + ) { + self.value = value + self.xAxisLabel = xAxisLabel + self.pointDescription = pointDescription + self.date = date + self.colour = colour + } +} + +public struct PieDataSet: SingleDataSet { + + public var id : UUID = UUID() + public var dataPoints : [PieChartDataPoint] + public var legendTitle : String + public var pointStyle : PointStyle + public var style : PieStyle + + public init(dataPoints : [PieChartDataPoint], + legendTitle : String, + pointStyle : PointStyle, + style : PieStyle + ) { + self.dataPoints = dataPoints + self.legendTitle = legendTitle + self.pointStyle = pointStyle + self.style = style + } + + public typealias Styling = PieStyle + public typealias DataPoint = PieChartDataPoint +} + +public struct PieStyle: 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? + + public init(colour : Color? = nil, + colours : [Color]? = nil, + stops : [GradientStop]? = nil, + startPoint : UnitPoint? = nil, + endPoint : UnitPoint? = nil + ) { + self.colourType = .colour + self.colour = colour + self.colours = colours + self.stops = stops + self.startPoint = startPoint + self.endPoint = endPoint + } +} diff --git a/Sources/SwiftUICharts/Shared/Models/ChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartStyle.swift similarity index 96% rename from Sources/SwiftUICharts/Shared/Models/ChartStyle.swift rename to Sources/SwiftUICharts/PieChart/Models/PieChartStyle.swift index e5823441..755a1675 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartStyle.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartStyle.swift @@ -1,14 +1,14 @@ // -// ChartStyle.swift +// PieChartStyle.swift // // -// Created by Will Dale on 12/01/2021. +// Created by Will Dale on 25/01/2021. // import SwiftUI /// Model for controlling the overall aesthetic of the chart. -public struct ChartStyle { +public struct PieChartStyle: CTPieChartStyle { /// Placement of the information box that appears on touch input. public var infoBoxPlacement : InfoBoxPlacement @@ -62,5 +62,3 @@ public struct ChartStyle { self.globalAnimation = globalAnimation } } - - diff --git a/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift b/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift new file mode 100644 index 00000000..f104f2ef --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift @@ -0,0 +1,39 @@ +// +// PieSegmentShape.swift +// +// +// Created by Will Dale on 24/01/2021. +// + +import SwiftUI + +internal struct PieSegmentShape: Shape, Identifiable { + + let data : PieChartDataPoint + var id : UUID { data.id } + var startAngle : Double + var amount : Double + + var animatableData: AnimatablePair { + get { AnimatablePair(startAngle, amount) } + set { + startAngle = newValue.first + amount = newValue.second + } + } + + 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/PieChart.swift b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift new file mode 100644 index 00000000..1611615b --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift @@ -0,0 +1,61 @@ +// +// PieChart.swift +// +// +// Created by Will Dale on 24/01/2021. +// + +import SwiftUI + +public struct PieChart: View { + + let chartData : PieChartData + + let pieSegments : [PieSegmentShape] + let strokeWidth : Double? + + @State var startAnimation : Bool = false + + public init(chartData : PieChartData, + strokeWidth: Double? = nil + ) { + self.chartData = chartData + + self.strokeWidth = strokeWidth + + var segments = [PieSegmentShape]() + let total = chartData.dataSets.dataPoints.reduce(0) { $0 + $1.value } + var startAngle = -Double.pi / 2 + + for data in chartData.dataSets.dataPoints { + let amount = .pi * 2 * (data.value / total) + let segment = PieSegmentShape(data: data, startAngle: startAngle, amount: amount) + segments.append(segment) + startAngle += amount + } + pieSegments = segments + } + + @ViewBuilder + var mask: some View { + if let strokeWidth = strokeWidth { + Circle() + .strokeBorder(Color(.white), lineWidth: CGFloat(strokeWidth)) + } else { + Circle() + } + } + + public var body: some View { + ZStack { + ForEach(pieSegments) { segment in + segment + .fill(segment.data.colour) + } + } + .mask(mask) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + } +} diff --git a/Sources/SwiftUICharts/Shared/Extras/AddLegends.swift b/Sources/SwiftUICharts/Shared/Extras/AddLegends.swift new file mode 100644 index 00000000..4a774a86 --- /dev/null +++ b/Sources/SwiftUICharts/Shared/Extras/AddLegends.swift @@ -0,0 +1,83 @@ +// +// Legends.swift +// +// +// Created by Will Dale on 26/01/2021. +// + +import SwiftUI + +internal struct AddLegends { + static func setupLine(chartData: inout T, dataSet: LineDataSet) { + if dataSet.style.colourType == .colour, + let colour = dataSet.style.colour + { + chartData.legends.append(LegendData(legend : dataSet.legendTitle, + colour : colour, + strokeStyle: dataSet.style.strokeStyle, + prioity : 1, + chartType : .line)) + + } else if dataSet.style.colourType == .gradientColour, + let colours = dataSet.style.colours + { + chartData.legends.append(LegendData(legend : dataSet.legendTitle, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: dataSet.style.strokeStyle, + prioity : 1, + chartType : .line)) + + } else if dataSet.style.colourType == .gradientStops, + let stops = dataSet.style.stops + { + chartData.legends.append(LegendData(legend : dataSet.legendTitle, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: dataSet.style.strokeStyle, + prioity : 1, + chartType : .line)) + } +// chartData.viewData.chartType = .line + } + + static func setupBar(chartData: inout T, dataSet: BarDataSet) { + + switch chartData.dataSets.style.colourFrom { + case .barStyle: + if dataSet.style.colourType == .colour, + let colour = dataSet.style.colour + { + chartData.legends.append(LegendData(legend : dataSet.legendTitle, + colour : colour, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if dataSet.style.colourType == .gradientColour, + let colours = dataSet.style.colours + { + chartData.legends.append(LegendData(legend : dataSet.legendTitle, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if dataSet.style.colourType == .gradientStops, + let stops = dataSet.style.stops + { + chartData.legends.append(LegendData(legend : dataSet.legendTitle, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } + case .dataPoints: + Text("") + } + } +} diff --git a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift index 799f5809..3feb302e 100644 --- a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift +++ b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift @@ -40,20 +40,13 @@ struct DataFunctions { } // MARK: - Single Data Set - static func dataSetMaxValue(from dataSets: [T]) -> Double { - var setHolder : [Double] = [] - for set in dataSets { - setHolder.append(set.dataPoints.max { $0.value < $1.value }?.value ?? 0) - } - return setHolder.max { $0 < $1 } ?? 0 + static func dataSetMaxValue(from dataSets: T) -> Double { + return dataSets.dataPoints.max { $0.value < $1.value }?.value ?? 0 + } - static func dataSetMinValue(from dataSets: [T]) -> 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 + static func dataSetMinValue(from dataSets: T) -> Double { + return dataSets.dataPoints.min { $0.value < $1.value }?.value ?? 0 } static func dataSetAverage(from dataSets: [T]) -> Double { @@ -66,18 +59,10 @@ struct DataFunctions { return sum / Double(setHolder.count) } - static func dataSetRange(from dataSets: [T]) -> Double { - var setMaxHolder : [Double] = [] - for set in dataSets { - setMaxHolder.append(set.dataPoints.max { $0.value < $1.value }?.value ?? 0) - } - let maxValue = setMaxHolder.max { $0 < $1 } ?? 0 + static func dataSetRange(from dataSets: T) -> Double { - var setMinHolder : [Double] = [] - for set in dataSets { - setMinHolder.append(set.dataPoints.min { $0.value < $1.value }?.value ?? 0) - } - let minValue = setMinHolder.min { $0 < $1 } ?? 0 + let maxValue = dataSets.dataPoints.max { $0.value < $1.value }?.value ?? 0 + let minValue = dataSets.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 diff --git a/Sources/SwiftUICharts/Shared/Extras/Enums.swift b/Sources/SwiftUICharts/Shared/Extras/Enums.swift index 4b4f8d5c..53cd5d45 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Enums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Enums.swift @@ -42,6 +42,8 @@ public enum ChartType { case line /// Bar Chart Type case bar + + case pie } // MARK: - Style diff --git a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift index 4b5709ba..e7274f75 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift @@ -15,6 +15,20 @@ extension View { else { self } } } +extension View { + @ViewBuilder + func `ifElse`(_ condition: Bool, + if ifTransform: (Self) -> TrueContent, + else elseTransform: (Self) -> FalseContent + ) -> some View { + + if condition { + ifTransform(self) + } else { + elseTransform(self) + } + } +} // https://www.hackingwithswift.com/quick-start/swiftui/how-to-start-an-animation-immediately-after-a-view-appears diff --git a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift index 763d855e..9ad9ef65 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift @@ -11,7 +11,7 @@ import Foundation public struct ChartViewData { /// Pass the type of chart being used to view modifiers. - var chartType : ChartType = .line +// var chartTypchartData.chartTypee : ChartType = .line /// If the chart has labels on the X axis, the Y axis needs a different layout var hasXAxisLabels : Bool = false diff --git a/Sources/SwiftUICharts/Shared/Models/LegendData.swift b/Sources/SwiftUICharts/Shared/Models/LegendData.swift index 20b29786..1f279478 100644 --- a/Sources/SwiftUICharts/Shared/Models/LegendData.swift +++ b/Sources/SwiftUICharts/Shared/Models/LegendData.swift @@ -10,8 +10,8 @@ import SwiftUI /// Data model for Legends public struct LegendData: CTColourStyle, Hashable { - var chartType : ChartType - public var colourType : ColourType + var chartType : ChartType + public var colourType : ColourType /// Text to be displayed var legend : String diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols.swift index a594b0b5..69ccadb7 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols.swift @@ -8,18 +8,19 @@ import SwiftUI public protocol ChartData: ObservableObject, Identifiable { - associatedtype Set : DataSet - associatedtype DataPoint : ChartDataPoint + associatedtype Set : DataSet + associatedtype DataPoint: ChartDataPoint + var id : UUID { get } var dataSets : Set { get set } var metadata : ChartMetadata? { get set } - var xAxisLabels : [String]? { get set } - var chartStyle : ChartStyle { get set } + var xAxisLabels : [String]? { get set } // Not pie var legends : [LegendData] { get set } var viewData : ChartViewData { get set } var noDataText : Text { get set } - var chartType : (ChartType, DataSetType) { get } + var chartType : (chartType: ChartType, dataSetType: DataSetType) { get } func legendOrder() -> [LegendData] + func getHeaderLocation() -> InfoBoxPlacement } extension ChartData { /// Sets the order the Legends are layed out in. @@ -28,24 +29,55 @@ extension ChartData { return legends.sorted { $0.prioity < $1.prioity} } } +public protocol LineAndBarChartData : ChartData { + associatedtype Style : CTLineAndBarChartStyle + + var chartStyle : Style { get set } + + +} +public protocol PieChartDataProtocol : ChartData { + associatedtype Style : CTPieChartStyle + + var chartStyle : Style { get set } +} + + + public protocol DataSet: Hashable, Identifiable { var id : ID { get } } - public protocol SingleDataSet: DataSet { - associatedtype Styling : CTColourStyle + associatedtype Styling : CTColourStyle associatedtype DataPoint : ChartDataPoint + var dataPoints : [DataPoint] { get set } var legendTitle : String { get set } var pointStyle : PointStyle { get set } var style : Styling { get set } } - public protocol MultiDataSet: DataSet { associatedtype DataSet : SingleDataSet + var dataSets : [DataSet] { get set } } + + +public protocol CTChartStyle { + var infoBoxPlacement : InfoBoxPlacement { get set } + var globalAnimation : Animation { get set } +} +public protocol CTLineAndBarChartStyle: CTChartStyle { + var xAxisGridStyle : GridStyle { get set } + var yAxisGridStyle : GridStyle { get set } + var xAxisLabelPosition : XAxisLabelPosistion { get set } + var xAxisLabelsFrom : LabelsFrom { get set } + var yAxisLabelPosition : YAxisLabelPosistion { get set } + var yAxisNumberOfLabels : Int { get set } +} +public protocol CTPieChartStyle: CTChartStyle {} + public protocol CTColourStyle { var colourType : ColourType { get set } var colour : Color? { get set } @@ -53,13 +85,15 @@ public protocol CTColourStyle { var stops : [GradientStop]? { get set } var startPoint : UnitPoint? { get set } var endPoint : UnitPoint? { get set } -// var ignoreZero : Bool { get set } } + + + public protocol ChartDataPoint: Hashable, Identifiable { - var id : ID { get } + var id : ID { get } var value : Double { get set } - var xAxisLabel : String? { get set } + var xAxisLabel : String? { get set } // Not Pie var pointDescription : String? { get set } var date : Date? { get set } } diff --git a/Sources/SwiftUICharts/Shared/Shapes/Marker.swift b/Sources/SwiftUICharts/Shared/Shapes/Marker.swift index a63c6b24..69aad5bd 100644 --- a/Sources/SwiftUICharts/Shared/Shapes/Marker.swift +++ b/Sources/SwiftUICharts/Shared/Shapes/Marker.swift @@ -42,6 +42,8 @@ internal struct Marker: Shape { case .bar: let y = rect.height / CGFloat(maxValue) pointY = rect.height - CGFloat(value) * y + case .pie: + pointY = 0 } let firstPoint = CGPoint(x: 0, diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/AxisBorders.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/AxisBorders.swift index 1f604771..20a4f5de 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/AxisBorders.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/AxisBorders.swift @@ -7,7 +7,7 @@ import SwiftUI -internal struct XAxisBorder: ViewModifier where T: ChartData { +internal struct XAxisBorder: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData: T @@ -37,7 +37,7 @@ internal struct XAxisBorder: ViewModifier where T: ChartData { } } -internal struct YAxisBorder: ViewModifier where T: ChartData { +internal struct YAxisBorder: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData: T @@ -68,11 +68,11 @@ internal struct YAxisBorder: ViewModifier where T: ChartData { } extension View { - internal func xAxisBorder(chartData: T) -> some View { + internal func xAxisBorder(chartData: T) -> some View { self.modifier(XAxisBorder(chartData: chartData)) } - internal func yAxisBorder(chartData: T) -> some View { + internal func yAxisBorder(chartData: T) -> some View { self.modifier(YAxisBorder(chartData: chartData)) } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index 7b8facce..58d7a501 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -6,101 +6,102 @@ // import SwiftUI + +internal struct HeaderBox: ViewModifier where T: ChartData { + + @ObservedObject var chartData: T + + let showTitle : Bool + let showSubtitle: Bool + + init(chartData : T, + showTitle : Bool = true, + showSubtitle : Bool = true + ) { + self.chartData = chartData + self.showTitle = showTitle + self.showSubtitle = showSubtitle + } + + 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) + } + } + } + + var touchOverlay: some View { + VStack(alignment: .trailing) { + if chartData.viewData.isTouchCurrent { + ForEach(chartData.viewData.touchOverlayInfo, id: \.self) { info in + Text("\(info.value, specifier: chartData.viewData.touchSpecifier)") + .font(.title3) + Text("\(info.pointDescription ?? "")") + .font(.subheadline) + } + } else { + Text("") + .font(.title3) + Text("") + .font(.subheadline) + } + } + } + + @ViewBuilder + internal func body(content: Content) -> some View { +// if chartData.isGreaterThanTwo { + #if !os(tvOS) + + if chartData.getHeaderLocation() == .floating { + VStack(alignment: .leading) { + titleBox + content + } + } else if chartData.getHeaderLocation() == .header { + VStack(alignment: .leading) { + HStack(spacing: 0) { + HStack(spacing: 0) { + titleBox + Spacer() + } + .frame(minWidth: 0, maxWidth: .infinity) + Spacer() + HStack(spacing: 0) { + Spacer() + touchOverlay + } + .frame(minWidth: 0, maxWidth: .infinity) + } + content + } + } + #elseif os(tvOS) + VStack(alignment: .leading) { + titleBox + content + } + #endif +// } else { content } + } +} -//internal struct HeaderBox: ViewModifier where T: ChartData { -// -// @ObservedObject var chartData: T -// -// let showTitle : Bool -// let showSubtitle: Bool -// -// init(chartData : T, -// showTitle : Bool = true, -// showSubtitle : Bool = true -// ) { -// self.chartData = chartData -// self.showTitle = showTitle -// self.showSubtitle = showSubtitle -// } -// -// 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) -// } -// } -// } -// -// var touchOverlay: some View { -// VStack(alignment: .trailing) { -// if chartData.viewData.isTouchCurrent { -// ForEach(chartData.viewData.touchOverlayInfo, id: \.self) { info in -// Text("\(info.value, specifier: chartData.viewData.touchSpecifier)") -// .font(.title3) -// Text("\(info.pointDescription ?? "")") -// .font(.subheadline) -// } -// } else { -// Text("") -// .font(.title3) -// Text("") -// .font(.subheadline) -// } -// } -// } -// -// @ViewBuilder -// internal func body(content: Content) -> some View { -//// if chartData.isGreaterThanTwo { -// #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() -// HStack(spacing: 0) { -// Spacer() -// touchOverlay -// } -// .frame(minWidth: 0, maxWidth: .infinity) -// } -// content -// } -// } -// #elseif os(tvOS) -// VStack(alignment: .leading) { -// titleBox -// content -// } -// #endif -//// } else { content } -// } -//} -// -//extension View { -// /// Displays the metadata about the chart -// /// - Returns: Chart title and subtitle. -// public func headerBox(chartData: T) -> some View { -// self.modifier(HeaderBox(chartData: chartData)) -// } -//} +extension View { + /// Displays the metadata about the chart + /// - Returns: Chart title and subtitle. + public func headerBox(chartData: T) -> some View { + self.modifier(HeaderBox(chartData: chartData)) + } +} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift index 2be9ffea..26e6b4eb 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift @@ -7,21 +7,21 @@ import SwiftUI -//internal struct Legends: ViewModifier where T: ChartData { -// -// @ObservedObject var chartData: T -// -// internal func body(content: Content) -> some View { -// VStack { -// content -// LegendView(chartData: chartData) -// } -// } -//} -//extension View { -// /// Displays legends under the chart. -// /// - Returns: Legends from the charts data and any markers. -// public func legends(chartData: T) -> some View { -// self.modifier(Legends(chartData: chartData)) -// } -//} +internal struct Legends: ViewModifier where T: ChartData { + + @ObservedObject var chartData: T + + internal func body(content: Content) -> some View { + VStack { + content + LegendView(chartData: chartData) + } + } +} +extension View { + /// Displays legends under the chart. + /// - Returns: Legends from the charts data and any markers. + public func legends(chartData: T) -> some View { + self.modifier(Legends(chartData: chartData)) + } +} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift index 7b437fdb..11a0a00c 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift @@ -7,7 +7,7 @@ import SwiftUI -internal struct XAxisGrid: ViewModifier where T: ChartData { +internal struct XAxisGrid: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData : T @@ -34,13 +34,13 @@ extension View { /** Adds vertical lines along the X axis. */ - public func xAxisGrid(chartData: T) -> some View { + public func xAxisGrid(chartData: T) -> some View { self.modifier(XAxisGrid(chartData: chartData)) } } -internal struct VerticalGridView: View where T: ChartData { +internal struct VerticalGridView: View where T: LineAndBarChartData { var chartData : T diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift index 10a8cb0d..342329b6 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift @@ -7,9 +7,15 @@ import SwiftUI -internal struct XAxisLabels: ViewModifier where T: ChartData { +internal struct XAxisLabels: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData: T + + internal init(chartData: T) { + self.chartData = chartData + + self.chartData.viewData.hasXAxisLabels = true + } @ViewBuilder internal var labels: some View { @@ -17,79 +23,32 @@ internal struct XAxisLabels: ViewModifier where T: ChartData { switch chartData.chartStyle.xAxisLabelsFrom { case .dataPoint: // ChartData -> DataPoints -> xAxisLabel - switch chartData.viewData.chartType { - case .line: - Text("") - if chartData.chartType == (.line, .multi) { - - let lineChartData = chartData as! MultiLineChartData - let dataSet = lineChartData.dataSets.dataSets - - HStack(spacing: 0) { - ForEach(dataSet[0].dataPoints) { data in - Text(data.xAxisLabel ?? "") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) - if data != dataSet[0].dataPoints[dataSet[0].dataPoints.count - 1] { - Spacer() - .frame(minWidth: 0, maxWidth: 500) - } - } - } - .padding(.horizontal, -4) - .onAppear { - chartData.viewData.hasXAxisLabels = true - } - - } else if chartData.chartType == (.line, .single) { - - let lineChartData = chartData as! LineChartData - let dataSet = lineChartData.dataSets - - HStack(spacing: 0) { - ForEach(dataSet.dataPoints) { data in - Text(data.xAxisLabel ?? "") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) - if data != dataSet.dataPoints[dataSet.dataPoints.count - 1] { - Spacer() - .frame(minWidth: 0, maxWidth: 500) - } - } - } - .padding(.horizontal, -4) - .onAppear { - chartData.viewData.hasXAxisLabels = true - } - } + + switch chartData.chartType { + case (.line, .single): + + XLabelSingleLineDataSet(chartData: chartData as! LineChartData) + + case (.line, .multi): + + XLabelMultiLineDataSet(chartData: chartData as! MultiLineChartData) + case (.bar, .single): - case .bar: - Text("hello") -// let barChartData = chartData as! BarChartData -// HStack(spacing: 0) { -// ForEach(barChartData.dataSets[0].dataPoints, id: \.self) { data in -// Spacer() -// .frame(minWidth: 0, maxWidth: 500) -// Text(data.xAxisLabel ?? "") -// .font(.caption) -// .lineLimit(1) -// .minimumScaleFactor(0.5) -// Spacer() -// .frame(minWidth: 0, maxWidth: 500) -// } -// } -// .onAppear { -// chartData.viewData.hasXAxisLabels = true -// } + XLabelSingleBarDataSet(chartData: chartData as! BarChartData) + + case (.bar, .multi): + + XLabelMultiBarDataSet(chartData: chartData as! MultiBarChartData) + + default: + Text("Should not be here") } case .chartData: - switch chartData.viewData.chartType { + switch chartData.chartType.chartType { case .line: // ChartData -> xAxisLabels if let labelArray = chartData.xAxisLabels { @@ -106,29 +65,25 @@ internal struct XAxisLabels: ViewModifier where T: ChartData { } } .padding(.horizontal, -4) - .onAppear { - chartData.viewData.hasXAxisLabels = true - } + } case .bar: - Text("Hello") -// 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 -// } -// } + 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) + } + } + } + case .pie: + Text("Should not be here") } } } @@ -153,7 +108,95 @@ internal struct XAxisLabels: ViewModifier where T: ChartData { extension View { /// Labels for the X axis. - public func xAxisLabels(chartData: T) -> some View { + public func xAxisLabels(chartData: T) -> some View { self.modifier(XAxisLabels(chartData: chartData)) } } + + +internal struct XLabelSingleLineDataSet: View where CD: LineChartData { + + let chartData: CD + + var body: some View { + + HStack(spacing: 0) { + ForEach(chartData.dataSets.dataPoints) { data in + Text(data.xAxisLabel ?? "") + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + if data != chartData.dataSets.dataPoints[chartData.dataSets.dataPoints.count - 1] { + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } + .padding(.horizontal, -4) + } + +} + +internal struct XLabelMultiLineDataSet: View where CD: MultiLineChartData { + + let chartData: CD + + var body: some View { + HStack(spacing: 0) { + ForEach(chartData.dataSets.dataSets[0].dataPoints) { data in + Text(data.xAxisLabel ?? "") + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + if data != chartData.dataSets.dataSets[0].dataPoints[chartData.dataSets.dataSets[0].dataPoints.count - 1] { + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } + .padding(.horizontal, -4) + } + +} +internal struct XLabelSingleBarDataSet: View where CD: BarChartData { + + let chartData: CD + + var body: some View { + + HStack(spacing: 0) { + ForEach(chartData.dataSets.dataPoints) { data in + Spacer() + .frame(minWidth: 0, maxWidth: 500) + Text(data.xAxisLabel ?? "") + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } +} + +internal struct XLabelMultiBarDataSet: View where CD: MultiBarChartData { + + let chartData: CD + + var body: some View { + HStack(spacing: 0) { + ForEach(chartData.dataSets.dataSets[0].dataPoints) { data in + Text(data.xAxisLabel ?? "") + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + if data != chartData.dataSets.dataSets[0].dataPoints[chartData.dataSets.dataSets[0].dataPoints.count - 1] { + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } + .padding(.horizontal, -4) + } + +} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift index b018670d..942cfa6b 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift @@ -7,7 +7,7 @@ import SwiftUI -internal struct YAxisGrid: ViewModifier where T: ChartData { +internal struct YAxisGrid: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData : T @@ -38,13 +38,13 @@ extension View { - Parameter numberOfLines: Number of lines subdividing the chart - Returns: View of evenly spaced horizontal lines */ - public func yAxisGrid(chartData: T) -> some View { + public func yAxisGrid(chartData: T) -> some View { self.modifier(YAxisGrid(chartData: chartData)) } } -internal struct HorizontalGridView: View where T: ChartData { +internal struct HorizontalGridView: View where T: LineAndBarChartData { var chartData : T diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift index ebbd7339..48a8d7e9 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift @@ -7,118 +7,158 @@ import SwiftUI -//internal struct YAxisLabels: ViewModifier where T: ChartData { -// -// @ObservedObject var chartData: T -// -// let specifier : String -// var labelsArray : [Double] { getLabels() } -// -// internal init(chartData: T, -// specifier: String -// ) { -// self.chartData = chartData -// 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) +internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { + + @ObservedObject var chartData: T + + let specifier : String + var labelsArray : [Double] { getLabels() } + + internal init(chartData: T, + specifier: String + ) { + self.chartData = chartData + self.specifier = specifier + + chartData.viewData.hasXAxisLabels = true + } + + 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) } + + } + + @ViewBuilder + internal func body(content: Content) -> some View { + switch chartData.chartStyle.yAxisLabelPosition { + case .leading: + HStack { +// if chartData.isGreaterThanTwo { + labels // } -// } -// 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.isGreaterThanTwo { -// labels -//// } -// content -// } -// case .trailing: -// HStack { -// content -//// if chartData.isGreaterThanTwo { -// 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] { -// var labels : [Double] = [Double]() -// let dataRange : Double = DataFunctions.dataSetRange(from: chartData.dataSets) -// let minValue : Double = DataFunctions.dataSetMinValue(from: chartData.dataSets) -// -// let range : Double = dataRange / Double(numberOfLabels) -// labels.append(minValue) -// for index in 1...numberOfLabels { -// labels.append(minValue + range * Double(index)) -// } -// return labels -// } -// internal func getYLabelsBarChart(_ numberOfLabels: Int) -> [Double] { -// var labels : [Double] = [Double]() -// let maxValue : Double = DataFunctions.dataSetMaxValue(from: chartData.dataSets) -// for index in 0...numberOfLabels { -// labels.append(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(chartData: T, specifier: String = "%.0f") -> some View { -// self.modifier(YAxisLabels(chartData: chartData, specifier: specifier)) -// } -//} + content + } + case .trailing: + HStack { + content +// if chartData.isGreaterThanTwo { + labels +// } + } + } + } + + internal func getLabels() -> [Double] { + let numberOfLabels = chartData.chartStyle.yAxisNumberOfLabels + + switch chartData.chartType { + case (.line, .single): + + let data = chartData as! LineChartData + return self.getYLabelsLineChart(dataSet: data.dataSets, numberOfLabels) + + case (.bar, .single): + + let data = chartData as! BarChartData + return self.getYLabelsBarChart(dataSet: data.dataSets, numberOfLabels) + + case (.line, .multi): + + let data = chartData as! MultiLineChartData + return self.getYLabelsMultiLineChart(dataSet: data.dataSets, numberOfLabels) + + case (.bar, .multi): + + let data = chartData as! MultiBarChartData + return self.getYLabelsMultiBarChart(dataSet: data.dataSets, numberOfLabels) + + default: + return [0.0] + } + } + + internal func getYLabelsLineChart(dataSet: DS, _ numberOfLabels: Int) -> [Double] { + var labels : [Double] = [Double]() + let dataRange : Double = DataFunctions.dataSetRange(from: dataSet) + let minValue : Double = DataFunctions.dataSetMinValue(from: dataSet) + + let range : Double = dataRange / Double(numberOfLabels) + labels.append(minValue) + for index in 1...numberOfLabels { + labels.append(minValue + range * Double(index)) + } + return labels + } + internal func getYLabelsBarChart(dataSet: DS, _ numberOfLabels: Int) -> [Double] { + var labels : [Double] = [Double]() + let maxValue : Double = DataFunctions.dataSetMaxValue(from: dataSet) + for index in 0...numberOfLabels { + labels.append(maxValue / Double(numberOfLabels) * Double(index)) + } + return labels + } + + internal func getYLabelsMultiLineChart(dataSet: DS, _ numberOfLabels: Int) -> [Double] { + var labels : [Double] = [Double]() + let dataRange : Double = DataFunctions.multiDataSetRange(from: dataSet) + let minValue : Double = DataFunctions.multiDataSetMinValue(from: dataSet) + + let range : Double = dataRange / Double(numberOfLabels) + labels.append(minValue) + for index in 1...numberOfLabels { + labels.append(minValue + range * Double(index)) + } + return labels + } + internal func getYLabelsMultiBarChart(dataSet: DS, _ numberOfLabels: Int) -> [Double] { + var labels : [Double] = [Double]() + let maxValue : Double = DataFunctions.multiDataSetMaxValue(from: dataSet) + for index in 0...numberOfLabels { + labels.append(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(chartData: T, specifier: String = "%.0f") -> some View { + self.modifier(YAxisLabels(chartData: chartData, specifier: specifier)) + } +} diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index 3516893c..f2db767a 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -104,6 +104,8 @@ internal struct LegendView: View where T: ChartData { // .font(.caption) // } // } + case .pie: + Text("") } } } From 4737b193732b1e368f7f0fee86ba81094c160b04 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 27 Jan 2021 18:32:16 +0000 Subject: [PATCH 008/152] Refactor functions into protocols. --- .../BarChart/Models/BarChartData.swift | 60 ++- .../BarChart/Models/MultiBarChartData.swift | 67 ++++ .../BarChart/Views/GroupedBarChart.swift | 42 +- .../LineChart/Models/LineChartData.swift | 65 ++- .../LineChart/Models/MultiLineChartData.swift | 70 +++- .../PieChart/Models/PieChartData.swift | 10 + .../Shared/Extras/DataFunctions.swift | 11 +- .../Shared/Models/Protocols.swift | 25 +- .../Shared/Shapes/PointShape.swift | 171 ++++---- .../Shared/ViewModifiers/PointMarkers.swift | 154 +++---- .../Shared/ViewModifiers/TouchOverlay.swift | 375 +++++++----------- .../Shared/ViewModifiers/XAxisLabels.swift | 111 +----- .../Shared/ViewModifiers/YAxisLabels.swift | 97 +---- .../Shared/ViewModifiers/YAxisPOI.swift | 224 +++++------ 14 files changed, 723 insertions(+), 759 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index 705f5b7a..36b560b6 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -54,10 +54,66 @@ public class BarChartData: LineAndBarChartData { return self.chartStyle.infoBoxPlacement } - public func legendOrder() -> [LegendData] { - return [LegendData]() + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { + var points : [BarChartDataPoint] = [] + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count) + let index : Int = Int((touchLocation.x) / xSection) + if index >= 0 && index < dataSets.dataPoints.count { + points.append(dataSets.dataPoints[index]) + } + return points } + public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { + var locations : [HashablePoint] = [] + + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count) + let ySection : CGFloat = chartSize.size.height / CGFloat(DataFunctions.dataSetMaxValue(from: dataSets)) + let index : Int = Int((touchLocation.x) / xSection) + + if index >= 0 && index < dataSets.dataPoints.count { + locations.append(HashablePoint(x: (CGFloat(index) * xSection) + (xSection / 2), + y: (chartSize.size.height - CGFloat(dataSets.dataPoints[index].value) * ySection))) + } + return locations + } + + public func getXAxidLabels() -> some View { + HStack(spacing: 0) { + ForEach(dataSets.dataPoints) { data in + Spacer() + .frame(minWidth: 0, maxWidth: 500) + Text(data.xAxisLabel ?? "") + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let maxValue: Double = DataFunctions.dataSetMaxValue(from: dataSets) + for index in 0...self.chartStyle.yAxisNumberOfLabels { + labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) + } + return labels + } + + public func getRange() -> Double { + DataFunctions.dataSetRange(from: dataSets) + } + public func getMinValue() -> Double { + DataFunctions.dataSetMinValue(from: dataSets) + } + public func getMaxValue() -> Double { + DataFunctions.dataSetMaxValue(from: dataSets) + } + public func getAverage() -> Double { + DataFunctions.dataSetAverage(from: dataSets) + } + public typealias Set = BarDataSet public typealias DataPoint = BarChartDataPoint } diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift index a8720ae2..5defa469 100644 --- a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift @@ -58,6 +58,73 @@ public class MultiBarChartData: LineAndBarChartData { return self.chartStyle.infoBoxPlacement } + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { + var points : [BarChartDataPoint] = [] + for dataSet in dataSets.dataSets { + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count) + let index : Int = Int((touchLocation.x) / xSection) + if index >= 0 && index < dataSet.dataPoints.count { + points.append(dataSet.dataPoints[index]) + } + } + return points + } + public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { + var locations : [HashablePoint] = [] + for dataSet in dataSets.dataSets { + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count) + let ySection : CGFloat = chartSize.size.height / CGFloat(DataFunctions.multiDataSetMaxValue(from: dataSets)) + + let index = Int((touchLocation.x) / xSection) + + if index >= 0 && index < dataSet.dataPoints.count { + locations.append(HashablePoint(x: (CGFloat(index) * xSection) + (xSection / 2), + y: (chartSize.size.height - CGFloat(dataSet.dataPoints[index].value) * ySection))) + } + } + return locations + } + public func getXAxidLabels() -> some View { + HStack(spacing: 100) { + ForEach(dataSets.dataSets) { dataSet in + HStack(spacing: 0) { + ForEach(dataSet.dataPoints) { data in + Text(data.xAxisLabel ?? "") + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + if data != dataSet.dataPoints[dataSet.dataPoints.count - 1] { + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } + } + } + .padding(.horizontal, -4) + } + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let maxValue : Double = DataFunctions.multiDataSetMaxValue(from: dataSets) + for index in 0...self.chartStyle.yAxisNumberOfLabels { + labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) + } + return labels + } + + public func getRange() -> Double { + DataFunctions.multiDataSetRange(from: dataSets) + } + public func getMinValue() -> Double { + DataFunctions.multiDataSetMinValue(from: dataSets) + } + public func getMaxValue() -> Double { + DataFunctions.multiDataSetMaxValue(from: dataSets) + } + public func getAverage() -> Double { + DataFunctions.multiDataSetAverage(from: dataSets) + } + public typealias Set = MultiBarDataSet public typealias DataPoint = BarChartDataPoint } diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index 9a5d6897..4ea830a4 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -25,31 +25,29 @@ public struct GroupedBarChart: View where ChartData: MultiBarChartDat HStack(spacing: 100) { ForEach(chartData.dataSets.dataSets) { dataSet in VStack { - HStack(spacing: 0) { - ForEach(dataSet.dataPoints) { dataPoint in - - switch dataSet.style.colourFrom { - case .barStyle: - - BarChartDataSetSubView(colourType: dataSet.style.colourType, - dataPoint: dataPoint, - style: dataSet.style, - chartStyle: chartData.chartStyle, - maxValue: maxValue) - - case .dataPoints: - - BarChartDataPointSubView(colourType: dataPoint.colourType, - dataPoint: dataPoint, - style: dataSet.style, - chartStyle: chartData.chartStyle, - maxValue: maxValue) + HStack(spacing: 0) { + ForEach(dataSet.dataPoints) { dataPoint in + switch dataSet.style.colourFrom { + case .barStyle: + + BarChartDataSetSubView(colourType: dataSet.style.colourType, + dataPoint: dataPoint, + style: dataSet.style, + chartStyle: chartData.chartStyle, + maxValue: maxValue) + + case .dataPoints: + + BarChartDataPointSubView(colourType: dataPoint.colourType, + dataPoint: dataPoint, + style: dataSet.style, + chartStyle: chartData.chartStyle, + maxValue: maxValue) + + } } } - } - Text(dataSet.legendTitle) - } } } diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index c0137c19..20751200 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -8,7 +8,7 @@ import SwiftUI /// The central model from which the chart is drawn. -public class LineChartData: LineAndBarChartData { +public class LineChartData: LineAndBarChartData, LineChartProtocol { public let id : UUID = UUID() @@ -67,7 +67,70 @@ public class LineChartData: LineAndBarChartData { public func getHeaderLocation() -> InfoBoxPlacement { return self.chartStyle.infoBoxPlacement } + + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [LineChartDataPoint] { + var points : [LineChartDataPoint] = [] + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count - 1) + let index = Int((touchLocation.x + (xSection / 2)) / xSection) + if index >= 0 && index < dataSets.dataPoints.count { + points.append(dataSets.dataPoints[index]) + } + return points + } + public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { + var locations : [HashablePoint] = [] + + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count - 1) + let ySection : CGFloat = chartSize.size.height / CGFloat(DataFunctions.dataSetRange(from: dataSets)) + let index : Int = Int((touchLocation.x + (xSection / 2)) / xSection) + if index >= 0 && index < dataSets.dataPoints.count { + locations.append(HashablePoint(x: CGFloat(index) * xSection, + y: (CGFloat(dataSets.dataPoints[index].value - DataFunctions.dataSetMinValue(from: dataSets)) * -ySection) + chartSize.size.height)) + } + return locations + } + public func getXAxidLabels() -> some View { + HStack(spacing: 0) { + ForEach(dataSets.dataPoints) { data in + Text(data.xAxisLabel ?? "") + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + if data != self.dataSets.dataPoints[self.dataSets.dataPoints.count - 1] { + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } + .padding(.horizontal, -4) + } + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let dataRange : Double = DataFunctions.dataSetRange(from: dataSets) + let minValue : Double = DataFunctions.dataSetMinValue(from: dataSets) + + let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) + labels.append(minValue) + for index in 1...self.chartStyle.yAxisNumberOfLabels { + labels.append(minValue + range * Double(index)) + } + return labels + } + + public func getRange() -> Double { + DataFunctions.dataSetRange(from: dataSets) + } + public func getMinValue() -> Double { + DataFunctions.dataSetMinValue(from: dataSets) + } + public func getMaxValue() -> Double { + DataFunctions.dataSetMaxValue(from: dataSets) + } + public func getAverage() -> Double { + DataFunctions.dataSetAverage(from: dataSets) + } + public typealias Set = LineDataSet public typealias DataPoint = LineChartDataPoint } diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift index 2cbd87bd..61459ca9 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -8,7 +8,7 @@ import SwiftUI /// The central model from which the chart is drawn. -public class MultiLineChartData: LineAndBarChartData { +public class MultiLineChartData: LineAndBarChartData, LineChartProtocol { public let id : UUID = UUID() @@ -67,9 +67,73 @@ public class MultiLineChartData: LineAndBarChartData { public func getHeaderLocation() -> InfoBoxPlacement { return self.chartStyle.infoBoxPlacement } + + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [LineChartDataPoint] { + var points : [LineChartDataPoint] = [] + for dataSet in dataSets.dataSets { + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) + let index = Int((touchLocation.x + (xSection / 2)) / xSection) + if index >= 0 && index < dataSet.dataPoints.count { + points.append(dataSet.dataPoints[index]) + } + } + return points + } + + public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { + + var locations : [HashablePoint] = [] + for dataSet in dataSets.dataSets { + + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) + let ySection : CGFloat = chartSize.size.height / CGFloat(DataFunctions.multiDataSetRange(from: dataSets)) + let index : Int = Int((touchLocation.x + (xSection / 2)) / xSection) + if index >= 0 && index < dataSet.dataPoints.count { + locations.append(HashablePoint(x: CGFloat(index) * xSection, + y: (CGFloat(dataSet.dataPoints[index].value - DataFunctions.multiDataSetMinValue(from: dataSets)) * -ySection) + chartSize.size.height)) + } + } + return locations + } + public func getXAxidLabels() -> some View { + HStack(spacing: 0) { + ForEach(dataSets.dataSets[0].dataPoints) { data in + Text(data.xAxisLabel ?? "") + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + if data != self.dataSets.dataSets[0].dataPoints[self.dataSets.dataSets[0].dataPoints.count - 1] { + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } + .padding(.horizontal, -4) + } + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let dataRange : Double = DataFunctions.multiDataSetRange(from: dataSets) + let minValue : Double = DataFunctions.multiDataSetMinValue(from: dataSets) + + let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) + labels.append(minValue) + for index in 1...self.chartStyle.yAxisNumberOfLabels { + labels.append(minValue + range * Double(index)) + } + return labels + } - public func getDataSet() -> MultiLineDataSet { - return self.dataSets + public func getRange() -> Double { + DataFunctions.multiDataSetRange(from: dataSets) + } + public func getMinValue() -> Double { + DataFunctions.multiDataSetMinValue(from: dataSets) + } + public func getMaxValue() -> Double { + DataFunctions.multiDataSetMaxValue(from: dataSets) + } + public func getAverage() -> Double { + DataFunctions.multiDataSetAverage(from: dataSets) } public typealias Set = MultiLineDataSet diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift index 92d68c1d..9d36cf02 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift @@ -40,6 +40,16 @@ public class PieChartData: PieChartDataProtocol { return self.chartStyle.infoBoxPlacement } + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [PieChartDataPoint] { + let points : [PieChartDataPoint] = [] + + return points + } + + public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { + return [HashablePoint(x: 0, y: 0)] + } + public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint } diff --git a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift index 3feb302e..3d87f150 100644 --- a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift +++ b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift @@ -49,14 +49,9 @@ struct DataFunctions { return dataSets.dataPoints.min { $0.value < $1.value }?.value ?? 0 } - static func dataSetAverage(from dataSets: [T]) -> 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) + static func dataSetAverage(from dataSets: T) -> Double { + let sum = dataSets.dataPoints.reduce(0) { $0 + $1.value } + return sum / Double(dataSets.dataPoints.count) } static func dataSetRange(from dataSets: T) -> Double { diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols.swift index 69ccadb7..61496c3f 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols.swift @@ -4,7 +4,7 @@ // // Created by Will Dale on 23/01/2021. // - + import SwiftUI public protocol ChartData: ObservableObject, Identifiable { @@ -21,6 +21,20 @@ public protocol ChartData: ObservableObject, Identifiable { var chartType : (chartType: ChartType, dataSetType: DataSetType) { get } func legendOrder() -> [LegendData] func getHeaderLocation() -> InfoBoxPlacement + + /// Gets the nearest data point to the touch location based on the X axis. + /// - Parameters: + /// - touchLocation: Current location of the touch + /// - chartSize: The size of the chart view as the parent view. + func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [DataPoint] + + /// Gets the location of the data point in the view. + /// - Parameters: + /// - touchLocation: Current location of the touch + /// - chartSize: The size of the chart view as the parent view. + func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] + + } extension ChartData { /// Sets the order the Legends are layed out in. @@ -29,12 +43,19 @@ extension ChartData { return legends.sorted { $0.prioity < $1.prioity} } } +public protocol LineChartProtocol {} public protocol LineAndBarChartData : ChartData { associatedtype Style : CTLineAndBarChartStyle + associatedtype Body : View var chartStyle : Style { get set } - + func getXAxidLabels() -> Body + func getYLabels() -> [Double] + func getRange() -> Double + func getMinValue() -> Double + func getMaxValue() -> Double + func getAverage() -> Double } public protocol PieChartDataProtocol : ChartData { associatedtype Style : CTPieChartStyle diff --git a/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift b/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift index f7927d0f..82d9bdda 100644 --- a/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift +++ b/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift @@ -7,105 +7,72 @@ import SwiftUI -//internal struct Point: Shape where T: DataSet { -// -// private let dataSet : T -// private let pointSize : CGFloat -// private let pointType : PointShape -// private let chartType : ChartType -// -// private let maxValue : Double -// private let minValue : Double -// private let range : Double -// -// internal init(dataSet : T, -// pointSize : CGFloat = 2, -// pointType : PointShape, -// chartType : ChartType, -// maxValue : Double, -// minValue : Double, -// range : Double -// ) { -// self.dataSet = dataSet -// self.pointSize = pointSize -// self.pointType = pointType -// self.chartType = chartType -// self.maxValue = maxValue -// self.minValue = minValue -// self.range = range -// } -// -// internal func path(in rect: CGRect) -> Path { -// var path = Path() -// -// switch chartType { -// case .line: -// lineChartDrawPoints(&path, rect, dataSet.dataPoints, minValue, range) -// case .bar: -// barChartDrawPoints(&path, rect, dataSet.dataPoints, minValue, maxValue) -// } -// return path -// } -// -// internal func barChartDrawPoints(_ path: inout Path, _ rect: CGRect, _ dataPoints: [ChartDataPoint], _ minValue: Double, _ maxValue: Double) { -// -// let x = rect.width / CGFloat(dataPoints.count) -// let y = rect.height / CGFloat(maxValue) -// -// for index in 0 ..< dataPoints.count { -// -// let pointX : CGFloat = (CGFloat(index) * x) - (pointSize / CGFloat(2)) + (x / 2) -// let pointY : CGFloat = (rect.height - (pointSize / CGFloat(2)) - CGFloat(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, _ dataPoints: [ChartDataPoint], _ minValue: Double, _ range: Double) { -// -// let x = rect.width / CGFloat(dataPoints.count-1) -// let y = rect.height / CGFloat(range) -// -// let firstPointX : CGFloat = (CGFloat(0) * x) - pointSize / CGFloat(2) -// let firstPointY : CGFloat = ((CGFloat(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) -// -// for index in 1 ..< dataPoints.count - 1 { -// let pointX : CGFloat = (CGFloat(index) * x) - pointSize / CGFloat(2) -// let pointY : CGFloat = ((CGFloat(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(dataPoints.count-1) * x) - pointSize / CGFloat(2) -// let lastPointY : CGFloat = ((CGFloat(dataPoints[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: 3, height: 3)) -// } -// } -//} +internal struct Point: Shape where T: SingleDataSet { + + private let dataSet : T + + private let maxValue : Double + private let minValue : Double + private let range : Double + + internal init(dataSet : T, + maxValue : Double, + minValue : Double, + range : Double + ) { + self.dataSet = dataSet + self.maxValue = maxValue + self.minValue = minValue + self.range = range + } + + internal func path(in rect: CGRect) -> Path { + var path = Path() + lineChartDrawPoints(&path, rect, dataSet.dataPoints, minValue, range) + return path + } + + internal func lineChartDrawPoints(_ path: inout Path, _ rect: CGRect, _ dataPoints: [DP], _ minValue: Double, _ range: Double) { + + let x = rect.width / CGFloat(dataPoints.count-1) + let y = rect.height / CGFloat(range) + + let firstPointX : CGFloat = (CGFloat(0) * x) - dataSet.pointStyle.pointSize / CGFloat(2) + let firstPointY : CGFloat = ((CGFloat(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) + pointSwitch(&path, firstPoint) + + for index in 1 ..< dataPoints.count - 1 { + let pointX : CGFloat = (CGFloat(index) * x) - dataSet.pointStyle.pointSize / CGFloat(2) + let pointY : CGFloat = ((CGFloat(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) + pointSwitch(&path, point) + } + + + let lastPointX : CGFloat = (CGFloat(dataPoints.count-1) * x) - dataSet.pointStyle.pointSize / CGFloat(2) + let lastPointY : CGFloat = ((CGFloat(dataPoints[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) + pointSwitch(&path, lastPoint) + } + + 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/Shared/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift index 4e9c6eb9..508f3445 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift @@ -7,74 +7,86 @@ import SwiftUI -//internal struct PointMarkers: ViewModifier where T: ChartData { -// -// @ObservedObject var chartData: T -// -// private let maxValue : Double -// private let minValue : Double -// private let range : Double -// -// internal init(chartData : T) { -// self.chartData = chartData -// self.maxValue = DataFunctions.dataSetMaxValue(from: chartData.dataSets) -// self.minValue = DataFunctions.dataSetMinValue(from: chartData.dataSets) -// self.range = DataFunctions.dataSetRange(from: chartData.dataSets) -// } -// internal func body(content: Content) -> some View { -// -// ZStack { -// content -// ForEach(chartData.dataSets, id: \.self) { dataSet in -//// if chartData.isGreaterThanTwo { -// switch dataSet.pointStyle.pointType { -// case .filled: -// Point(dataSet : dataSet, -// pointSize : dataSet.pointStyle.pointSize, -// pointType : dataSet.pointStyle.pointShape, -// chartType : chartData.viewData.chartType, -// maxValue : maxValue, -// minValue : minValue, -// range : range) -// .fill(dataSet.pointStyle.fillColour) -// case .outline: Text("") -// Point(dataSet : dataSet, -// pointSize : dataSet.pointStyle.pointSize, -// pointType : dataSet.pointStyle.pointShape, -// chartType : chartData.viewData.chartType, -// maxValue : maxValue, -// minValue : minValue, -// range : range) -// .stroke(dataSet.pointStyle.borderColour, lineWidth: dataSet.pointStyle.lineWidth) -// case .filledOutLine: Text("") -// Point(dataSet : dataSet, -// pointSize : dataSet.pointStyle.pointSize, -// pointType : dataSet.pointStyle.pointShape, -// chartType : chartData.viewData.chartType, -// maxValue : maxValue, -// minValue : minValue, -// range : range) -// .stroke(dataSet.pointStyle.borderColour, lineWidth: dataSet.pointStyle.lineWidth) -// .background(Point(dataSet : dataSet, -// pointSize : dataSet.pointStyle.pointSize, -// pointType : dataSet.pointStyle.pointShape, -// chartType : chartData.viewData.chartType, -// maxValue : maxValue, -// minValue : minValue, -// range : range) -// .foregroundColor(dataSet.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(chartData: T) -> some View { -// self.modifier(PointMarkers(chartData: chartData)) -// } -//} +internal struct PointMarkers: ViewModifier where T: LineAndBarChartData & LineChartProtocol { + + @ObservedObject var chartData: T + + private let maxValue : Double + private let minValue : Double + private let range : Double + + internal init(chartData : T) { + self.chartData = chartData + + self.maxValue = chartData.getMaxValue() + self.minValue = chartData.getMinValue() + self.range = chartData.getRange() + } + internal func body(content: Content) -> some View { + + ZStack { + content + if chartData.chartType.dataSetType == .single { + + let data = chartData as! LineChartData + PointsSubView(dataSets: data.dataSets, maxValue: maxValue, minValue: minValue, range: range) + + } else if chartData.chartType.dataSetType == .multi { + + let data = chartData as! MultiLineChartData + ForEach(data.dataSets.dataSets, id: \.self) { dataSet in +// if chartData.isGreaterThanTwo { + PointsSubView(dataSets: dataSet, maxValue: maxValue, minValue: minValue, range: range) +// } + } + } + } + } +} + +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(chartData: T) -> some View { + self.modifier(PointMarkers(chartData: chartData)) + } +} + +struct PointsSubView: View { + + let dataSets: LineDataSet + let maxValue : Double + let minValue : Double + let range : Double + + var body: some View { + switch dataSets.pointStyle.pointType { + case .filled: + Point(dataSet : dataSets, + maxValue : maxValue, + minValue : minValue, + range : range) + .fill(dataSets.pointStyle.fillColour) + case .outline: + Point(dataSet : dataSets, + maxValue : maxValue, + minValue : minValue, + range : range) + .stroke(dataSets.pointStyle.borderColour, lineWidth: dataSets.pointStyle.lineWidth) + case .filledOutLine: + Point(dataSet : dataSets, + maxValue : maxValue, + minValue : minValue, + range : range) + .stroke(dataSets.pointStyle.borderColour, lineWidth: dataSets.pointStyle.lineWidth) + .background(Point(dataSet : dataSets, + maxValue : maxValue, + minValue : minValue, + range : range) + .foregroundColor(dataSets.pointStyle.fillColour) + ) + } + } + +} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 3f25e4d2..5394447a 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -7,246 +7,151 @@ import SwiftUI -//#if !os(tvOS) -///// Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information. -//internal struct TouchOverlay: ViewModifier where T: ChartData { -// -// @ObservedObject var chartData: T -// -// /// Decimal precision for labels -// private let specifier : String -// private let touchMarkerLineWidth : CGFloat = 1 // API? -// -// /// Boolean that indicates whether touch is currently being detected -// @State private var isTouchCurrent : Bool = false -// /// Current location of the touch input -// @State private var touchLocation : CGPoint = CGPoint(x: 0, y: 0) -// /// The data point closest to the touch input -// @State private var selectedPoints : [ChartDataPoint] = [] -// /// The location for the nearest data point to the touch input -// @State private var pointLocations : [HashablePoint] = [HashablePoint(x: 0, y: 0)] -// /// Frame information of the data point information box -// @State private var boxFrame : CGRect = CGRect(x: 0, y: 0, width: 0, height: 50) -// /// Placement of the data point information box -// @State private var boxLocation : CGPoint = CGPoint(x: 0, y: 0) -// /// Placement of place the markers intersecting the data points location -// @State private var markerLocation : CGPoint = CGPoint(x: 0, y: 0) -// -// /// Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information. -// /// - Parameters: -// /// - chartData: -// /// - specifier: Decimal precision for labels -// internal init(chartData: T, -// specifier: String -// ) { -// self.chartData = chartData -// self.specifier = specifier -// } -// -// internal func body(content: Content) -> some View { -//// if chartData.isGreaterThanTwo { -// GeometryReader { geo in -// ZStack { -// content -// .gesture( -// DragGesture(minimumDistance: 0) -// .onChanged { (value) in -// touchLocation = value.location -// isTouchCurrent = true -// -// switch chartData.viewData.chartType { -// case .line: -// getPointLocationLineChart(dataSets : chartData.dataSets, -// touchLocation : touchLocation, -// chartSize : geo) -// getDataPointLineChart(dataSets : chartData.dataSets, -// touchLocation : touchLocation, -// chartSize : geo) -// case .bar: -// getPointLocationBarChart(dataSets: chartData.dataSets, -// touchLocation: touchLocation, -// chartSize: geo) -// getDataPointBarChart(dataSets: chartData.dataSets, -// touchLocation: touchLocation, -// chartSize: geo) -// } -// -// if chartData.chartStyle.infoBoxPlacement == .floating { -// setBoxLocationation(boxFrame: boxFrame, chartSize: geo) -// markerLocation.x = setMarkerXLocation(chartSize: geo) -// markerLocation.y = setMarkerYLocation(chartSize: geo) -// } else if chartData.chartStyle.infoBoxPlacement == .header { -// chartData.viewData.isTouchCurrent = true -// chartData.viewData.touchOverlayInfo = selectedPoints -// } -// } -// .onEnded { _ in -// isTouchCurrent = false -// chartData.viewData.isTouchCurrent = false -// } -// ) -// if isTouchCurrent { -// ForEach(pointLocations, id: \.self) { location in -// TouchOverlayMarker(position: location) -// .stroke(Color(.gray), lineWidth: touchMarkerLineWidth) -// } -// if chartData.chartStyle.infoBoxPlacement == .floating { -// TouchOverlayBox(selectedPoints: selectedPoints, specifier: specifier, boxFrame: $boxFrame) -// .position(x: boxLocation.x, y: 0 + (boxFrame.height / 2)) -// } -// } -// } -// } -//// } else { content } -// } -// -// // MARK: - Line Chart -// /// Gets the nearest data point to the touch location based on the X axis. -// /// - Parameters: -// /// - touchLocation: Current location of the touch -// /// - chartSize: The size of the chart view as the parent view. -// internal func getDataPointLineChart(dataSets : [U], -// touchLocation : CGPoint, -// chartSize : GeometryProxy) { // -> [ChartDataPoint] -// var points : [ChartDataPoint] = [] -// for dataSet in dataSets { -// let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) -// let index = Int((touchLocation.x + (xSection / 2)) / xSection) -// if index >= 0 && index < dataSet.dataPoints.count { -// points.append(dataSet.dataPoints[index]) -// } -// } -// self.selectedPoints = points -// } -// /// Gets the location of the data point in the view. For Line Chart -// /// - Parameters: -// /// - touchLocation: Current location of the touch -// /// - chartSize: The size of the chart view as the parent view. -// internal func getPointLocationLineChart(dataSets: [U], -// touchLocation: CGPoint, -// chartSize: GeometryProxy) { // -> CGPoint -// -// let range = DataFunctions.dataSetRange(from: dataSets) -// let minValue = DataFunctions.dataSetMinValue(from: dataSets) -// -// var locations : [HashablePoint] = [] -// for dataSet in dataSets { -// -// let dataPointCount : Int = dataSet.dataPoints.count -// let xSection : CGFloat = chartSize.size.width / CGFloat(dataPointCount - 1) -// let ySection : CGFloat = chartSize.size.height / CGFloat(range) -// let index = Int((touchLocation.x + (xSection / 2)) / xSection) -// if index >= 0 && index < dataPointCount { -// locations.append(HashablePoint(x: CGFloat(index) * xSection, -// y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.size.height)) -// } -// } -// self.pointLocations = locations -// } -// -// // MARK: - Bar Chart -// /// Gets the nearest data point to the touch location based on the X axis. -// /// - Parameters: -// /// - touchLocation: Current location of the touch -// /// - chartSize: The size of the chart view as the parent view. -// internal func getDataPointBarChart(dataSets : [U], -// touchLocation : CGPoint, -// chartSize : GeometryProxy) { // -> [ChartDataPoint] -// var points : [ChartDataPoint] = [] -// for dataSet in dataSets { -// let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count) -// let index : Int = Int((touchLocation.x) / xSection) -// if index >= 0 && index < dataSet.dataPoints.count { -// points.append(dataSet.dataPoints[index]) -// } -// } -// self.selectedPoints = points -// } -// -// /// Gets the location of the data point in the view. For BarChart -// /// - Parameters: -// /// - touchLocation: Current location of the touch -// /// - chartSize: The size of the chart view as the parent view. -// internal func getPointLocationBarChart(dataSets: [U], -// touchLocation: CGPoint, -// chartSize: GeometryProxy) { // -> CGPoint -// var locations : [HashablePoint] = [] -// for dataSet in dataSets { -// let dataPointCount : Int = dataSet.dataPoints.count -// let xSection : CGFloat = chartSize.size.width / CGFloat(dataPointCount) -// let ySection : CGFloat = chartSize.size.height / CGFloat(DataFunctions.dataSetMaxValue(from: dataSets)) -// -// let index = Int((touchLocation.x) / xSection) -// -// if index >= 0 && index < dataPointCount { -// locations.append(HashablePoint(x: (CGFloat(index) * xSection) + (xSection / 2), -// y: (chartSize.size.height - CGFloat(dataSet.dataPoints[index].value) * ySection))) -// } -// } -// self.pointLocations = locations -// } -// -// // MARK: - Both -// /// Sets the 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(boxFrame: CGRect, chartSize: GeometryProxy) { -// if touchLocation.x < chartSize.frame(in: .local).minX + (boxFrame.width / 2) { -// boxLocation.x = chartSize.frame(in: .local).minX + (boxFrame.width / 2) -// } else if touchLocation.x > chartSize.frame(in: .local).maxX - (boxFrame.width / 2) { -// boxLocation.x = chartSize.frame(in: .local).maxX - (boxFrame.width / 2) -// } else { -// boxLocation.x = touchLocation.x -// } -// } -// /// Sets the X axis marker location while keeping it within the parent view. -// /// - Parameter chartSize: The size of the chart view as the parent view. -// /// - Returns: Position of the marker. -// internal func setMarkerXLocation(chartSize: GeometryProxy) -> CGFloat { -// if touchLocation.x < chartSize.frame(in: .local).minX { -// return chartSize.frame(in: .local).minX -// } else if touchLocation.x > chartSize.frame(in: .local).maxX { -// return chartSize.frame(in: .local).maxX -// } else { -// return touchLocation.x -// } -// } -// /// Sets the Y axis marker location while keeping it within the parent view. -// /// - Parameter chartSize: The size of the chart view as the parent view. -// /// - Returns: Position of the marker. -// internal func setMarkerYLocation(chartSize: GeometryProxy) -> CGFloat { -// if touchLocation.y < chartSize.frame(in: .local).minY { -// return chartSize.frame(in: .local).minY -// } else if touchLocation.y > chartSize.frame(in: .local).maxY { -// return chartSize.frame(in: .local).maxY -// } else { -// return touchLocation.y -// } -// } -//} -//#endif -// -//extension View { -// #if !os(tvOS) -// /// 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(chartData: T, specifier: String = "%.0f") -> some View { -// self.modifier(TouchOverlay(chartData: chartData, specifier: specifier)) -// } -// #elseif os(tvOS) -// public func touchOverlay(specifier: String = "%.0f") -> some View { -// self.modifier(EmptyModifier()) -// } -// #endif -//} +#if !os(tvOS) +/// Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information. +internal struct TouchOverlay: ViewModifier where T: ChartData { + + @ObservedObject var chartData: T + + /// Decimal precision for labels + private let specifier : String + private let touchMarkerLineWidth : CGFloat = 1 // API? + + /// Boolean that indicates whether touch is currently being detected + @State private var isTouchCurrent : Bool = false + /// Current location of the touch input + @State private var touchLocation : CGPoint = CGPoint(x: 0, y: 0) + /// The data point closest to the touch input + @State private var selectedPoints : [T.DataPoint] = [] + /// The location for the nearest data point to the touch input + @State private var pointLocations : [HashablePoint] = [HashablePoint(x: 0, y: 0)] + /// Frame information of the data point information box + @State private var boxFrame : CGRect = CGRect(x: 0, y: 0, width: 0, height: 50) + /// Placement of the data point information box + @State private var boxLocation : CGPoint = CGPoint(x: 0, y: 0) + /// Placement of place the markers intersecting the data points location + @State private var markerLocation : CGPoint = CGPoint(x: 0, y: 0) + + /// Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information. + /// - Parameters: + /// - chartData: + /// - specifier: Decimal precision for labels + internal init(chartData: T, + specifier: String + ) { + self.chartData = chartData + self.specifier = specifier + } + + internal func body(content: Content) -> some View { +// if chartData.isGreaterThanTwo { + GeometryReader { geo in + ZStack { + content + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { (value) in + touchLocation = value.location + isTouchCurrent = true + + self.selectedPoints = chartData.getDataPoint(touchLocation: touchLocation, + chartSize: geo) + self.pointLocations = chartData.getPointLocation(touchLocation: touchLocation, + chartSize: geo) + + + if chartData.getHeaderLocation() == .floating { + + setBoxLocationation(boxFrame: boxFrame, chartSize: geo) + markerLocation.x = setMarkerXLocation(chartSize: geo) + markerLocation.y = setMarkerYLocation(chartSize: geo) + + } else if chartData.getHeaderLocation() == .header { + + chartData.viewData.isTouchCurrent = true + chartData.viewData.touchOverlayInfo = selectedPoints + } + } + .onEnded { _ in + isTouchCurrent = false + chartData.viewData.isTouchCurrent = false + } + ) + if isTouchCurrent { + ForEach(pointLocations, id: \.self) { location in + TouchOverlayMarker(position: location) + .stroke(Color(.gray), lineWidth: touchMarkerLineWidth) + } + if chartData.getHeaderLocation() == .floating { + TouchOverlayBox(selectedPoints: selectedPoints, specifier: specifier, boxFrame: $boxFrame) + .position(x: boxLocation.x, y: 0 + (boxFrame.height / 2)) + } + } + } + } +// } else { content } + } + + /// Sets the 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(boxFrame: CGRect, chartSize: GeometryProxy) { + if touchLocation.x < chartSize.frame(in: .local).minX + (boxFrame.width / 2) { + boxLocation.x = chartSize.frame(in: .local).minX + (boxFrame.width / 2) + } else if touchLocation.x > chartSize.frame(in: .local).maxX - (boxFrame.width / 2) { + boxLocation.x = chartSize.frame(in: .local).maxX - (boxFrame.width / 2) + } else { + boxLocation.x = touchLocation.x + } + } + /// Sets the X axis marker location while keeping it within the parent view. + /// - Parameter chartSize: The size of the chart view as the parent view. + /// - Returns: Position of the marker. + internal func setMarkerXLocation(chartSize: GeometryProxy) -> CGFloat { + if touchLocation.x < chartSize.frame(in: .local).minX { + return chartSize.frame(in: .local).minX + } else if touchLocation.x > chartSize.frame(in: .local).maxX { + return chartSize.frame(in: .local).maxX + } else { + return touchLocation.x + } + } + /// Sets the Y axis marker location while keeping it within the parent view. + /// - Parameter chartSize: The size of the chart view as the parent view. + /// - Returns: Position of the marker. + internal func setMarkerYLocation(chartSize: GeometryProxy) -> CGFloat { + if touchLocation.y < chartSize.frame(in: .local).minY { + return chartSize.frame(in: .local).minY + } else if touchLocation.y > chartSize.frame(in: .local).maxY { + return chartSize.frame(in: .local).maxY + } else { + return touchLocation.y + } + } +} +#endif + +extension View { + #if !os(tvOS) + /// 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(chartData: T, specifier: String = "%.0f") -> some View { + self.modifier(TouchOverlay(chartData: chartData, specifier: specifier)) + } + #elseif os(tvOS) + public func touchOverlay(specifier: String = "%.0f") -> some View { + self.modifier(EmptyModifier()) + } + #endif +} public struct HashablePoint: Hashable { - + public let x : CGFloat public let y : CGFloat - + public init(x: CGFloat, y: CGFloat) { self.x = x self.y = y diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift index 342329b6..f4095cb7 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift @@ -24,28 +24,7 @@ internal struct XAxisLabels: ViewModifier where T: LineAndBarChartData { case .dataPoint: // ChartData -> DataPoints -> xAxisLabel - switch chartData.chartType { - case (.line, .single): - - XLabelSingleLineDataSet(chartData: chartData as! LineChartData) - - case (.line, .multi): - - XLabelMultiLineDataSet(chartData: chartData as! MultiLineChartData) - - case (.bar, .single): - - XLabelSingleBarDataSet(chartData: chartData as! BarChartData) - - case (.bar, .multi): - - XLabelMultiBarDataSet(chartData: chartData as! MultiBarChartData) - - default: - Text("Should not be here") - } - - + chartData.getXAxidLabels() case .chartData: switch chartData.chartType.chartType { @@ -112,91 +91,3 @@ extension View { self.modifier(XAxisLabels(chartData: chartData)) } } - - -internal struct XLabelSingleLineDataSet: View where CD: LineChartData { - - let chartData: CD - - var body: some View { - - HStack(spacing: 0) { - ForEach(chartData.dataSets.dataPoints) { data in - Text(data.xAxisLabel ?? "") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) - if data != chartData.dataSets.dataPoints[chartData.dataSets.dataPoints.count - 1] { - Spacer() - .frame(minWidth: 0, maxWidth: 500) - } - } - } - .padding(.horizontal, -4) - } - -} - -internal struct XLabelMultiLineDataSet: View where CD: MultiLineChartData { - - let chartData: CD - - var body: some View { - HStack(spacing: 0) { - ForEach(chartData.dataSets.dataSets[0].dataPoints) { data in - Text(data.xAxisLabel ?? "") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) - if data != chartData.dataSets.dataSets[0].dataPoints[chartData.dataSets.dataSets[0].dataPoints.count - 1] { - Spacer() - .frame(minWidth: 0, maxWidth: 500) - } - } - } - .padding(.horizontal, -4) - } - -} -internal struct XLabelSingleBarDataSet: View where CD: BarChartData { - - let chartData: CD - - var body: some View { - - HStack(spacing: 0) { - ForEach(chartData.dataSets.dataPoints) { data in - Spacer() - .frame(minWidth: 0, maxWidth: 500) - Text(data.xAxisLabel ?? "") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) - Spacer() - .frame(minWidth: 0, maxWidth: 500) - } - } - } -} - -internal struct XLabelMultiBarDataSet: View where CD: MultiBarChartData { - - let chartData: CD - - var body: some View { - HStack(spacing: 0) { - ForEach(chartData.dataSets.dataSets[0].dataPoints) { data in - Text(data.xAxisLabel ?? "") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) - if data != chartData.dataSets.dataSets[0].dataPoints[chartData.dataSets.dataSets[0].dataPoints.count - 1] { - Spacer() - .frame(minWidth: 0, maxWidth: 500) - } - } - } - .padding(.horizontal, -4) - } - -} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift index 48a8d7e9..b6a5b2c6 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift @@ -12,7 +12,7 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData: T let specifier : String - var labelsArray : [Double] { getLabels() } + var labelsArray : [Double] { chartData.getYLabels() } internal init(chartData: T, specifier: String @@ -20,9 +20,16 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { self.chartData = chartData self.specifier = specifier - chartData.viewData.hasXAxisLabels = true + chartData.viewData.hasYAxisLabels = true } - + + internal var textAsSpacer: some View { + Text("") + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + } + internal var labels: some View { let labelsAndTop = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .top let labelsAndBottom = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .bottom @@ -30,12 +37,7 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { return VStack { if labelsAndTop { - Text("") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) - Spacer() - .frame(minHeight: 0, maxHeight: 500) + textAsSpacer } ForEach((0...numberOfLabels).reversed(), id: \.self) { i in Text("\(labelsArray[i], specifier: specifier)") @@ -48,15 +50,11 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { } } if labelsAndBottom { - Text("") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) + textAsSpacer } } .if(labelsAndBottom) { $0.padding(.top, -8) } .if(labelsAndTop) { $0.padding(.bottom, -8) } - } @ViewBuilder @@ -78,77 +76,6 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { } } } - - internal func getLabels() -> [Double] { - let numberOfLabels = chartData.chartStyle.yAxisNumberOfLabels - - switch chartData.chartType { - case (.line, .single): - - let data = chartData as! LineChartData - return self.getYLabelsLineChart(dataSet: data.dataSets, numberOfLabels) - - case (.bar, .single): - - let data = chartData as! BarChartData - return self.getYLabelsBarChart(dataSet: data.dataSets, numberOfLabels) - - case (.line, .multi): - - let data = chartData as! MultiLineChartData - return self.getYLabelsMultiLineChart(dataSet: data.dataSets, numberOfLabels) - - case (.bar, .multi): - - let data = chartData as! MultiBarChartData - return self.getYLabelsMultiBarChart(dataSet: data.dataSets, numberOfLabels) - - default: - return [0.0] - } - } - - internal func getYLabelsLineChart(dataSet: DS, _ numberOfLabels: Int) -> [Double] { - var labels : [Double] = [Double]() - let dataRange : Double = DataFunctions.dataSetRange(from: dataSet) - let minValue : Double = DataFunctions.dataSetMinValue(from: dataSet) - - let range : Double = dataRange / Double(numberOfLabels) - labels.append(minValue) - for index in 1...numberOfLabels { - labels.append(minValue + range * Double(index)) - } - return labels - } - internal func getYLabelsBarChart(dataSet: DS, _ numberOfLabels: Int) -> [Double] { - var labels : [Double] = [Double]() - let maxValue : Double = DataFunctions.dataSetMaxValue(from: dataSet) - for index in 0...numberOfLabels { - labels.append(maxValue / Double(numberOfLabels) * Double(index)) - } - return labels - } - - internal func getYLabelsMultiLineChart(dataSet: DS, _ numberOfLabels: Int) -> [Double] { - var labels : [Double] = [Double]() - let dataRange : Double = DataFunctions.multiDataSetRange(from: dataSet) - let minValue : Double = DataFunctions.multiDataSetMinValue(from: dataSet) - - let range : Double = dataRange / Double(numberOfLabels) - labels.append(minValue) - for index in 1...numberOfLabels { - labels.append(minValue + range * Double(index)) - } - return labels - } - internal func getYLabelsMultiBarChart(dataSet: DS, _ numberOfLabels: Int) -> [Double] { - var labels : [Double] = [Double]() - let maxValue : Double = DataFunctions.multiDataSetMaxValue(from: dataSet) - for index in 0...numberOfLabels { - labels.append(maxValue / Double(numberOfLabels) * Double(index)) - } - return labels - } } extension View { diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift index 3a485b1b..5df064dd 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift @@ -8,122 +8,110 @@ import SwiftUI /// Configurable Point of interest -//internal struct YAxisPOI: ViewModifier where T: ChartData { -// -// @ObservedObject var chartData: T -// -// private let markerName : String -// private var markerValue : Double -// private let lineColour : Color -// private let strokeStyle : StrokeStyle -// -// private let range : Double -// private let minValue : Double -// private let maxValue : Double -// -// internal init(chartData : T, -// markerName : String, -// markerValue : Double = 0, -// lineColour : Color, -// strokeStyle : StrokeStyle, -// isAverage : Bool -// ) { -// self.chartData = chartData -// self.markerName = markerName -// self.markerValue = markerValue -// self.lineColour = lineColour -// self.strokeStyle = strokeStyle -// -// self.markerValue = isAverage ? DataFunctions.dataSetAverage(from: chartData.dataSets) : markerValue -// //Line -// self.range = DataFunctions.dataSetRange(from: chartData.dataSets) -// self.minValue = DataFunctions.dataSetMinValue(from: chartData.dataSets) -// -// -// // Bar -// /* -// -// -// THIS WILL NEED FIXING !!!! -// -// -// */ -// self.maxValue = DataFunctions.maxValue(dataPoints: chartData.dataSets[0].dataPoints) -// -// } -// -// internal func body(content: Content) -> some View { -// ZStack { -// content -//// if chartData.isGreaterThanTwo { -// Marker(value : markerValue, -// range : range, -// minValue : minValue, -// maxValue : maxValue, -// 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)) +internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { + + @ObservedObject var chartData: T + + private let markerName : String + private var markerValue : Double + private let lineColour : Color + private let strokeStyle : StrokeStyle + + private let range : Double + private let minValue : Double + private let maxValue : Double + + internal init(chartData : T, + markerName : String, + markerValue : Double = 0, + lineColour : Color, + strokeStyle : StrokeStyle, + isAverage : Bool + ) { + self.chartData = chartData + self.markerName = markerName + self.lineColour = lineColour + self.strokeStyle = strokeStyle + + self.markerValue = isAverage ? chartData.getAverage() : markerValue + self.range = chartData.getRange() + self.minValue = chartData.getMinValue() + self.maxValue = chartData.getMaxValue() + + } + + internal func body(content: Content) -> some View { + ZStack { + content +// if chartData.isGreaterThanTwo { + Marker(value : markerValue, + range : range, + minValue : minValue, + maxValue : maxValue, + chartType : chartData.chartType.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)) + } // } -//// } -// } -// } -// } -//} -// -//extension View { -// /// Shows a marker line at chosen point. -// /// - Parameters: -// /// - markerName: Title of marker, for the legend -// /// - markerValue : Chosen point. -// /// - lineColour: Line Colour -// /// - strokeStyle: Style of Stroke -// /// - Returns: A marker line at the average of all the data points. -// public func yAxisPOI(chartData : T, -// markerName : String, -// markerValue : Double, -// 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, -// 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 -// /// - lineColour: Line Colour -// /// - strokeStyle: Style of Stroke -// /// - Returns: A marker line at the average of all the data points. -// public func averageLine(chartData : T, -// markerName : String = "Average", -// 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, -// lineColour : lineColour, -// strokeStyle : strokeStyle, -// isAverage : true)) -// } -//} + } + } + } +} + +extension View { + /// Shows a marker line at chosen point. + /// - Parameters: + /// - markerName: Title of marker, for the legend + /// - markerValue : Chosen point. + /// - lineColour: Line Colour + /// - strokeStyle: Style of Stroke + /// - Returns: A marker line at the average of all the data points. + public func yAxisPOI(chartData : T, + markerName : String, + markerValue : Double, + 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, + 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 + /// - lineColour: Line Colour + /// - strokeStyle: Style of Stroke + /// - Returns: A marker line at the average of all the data points. + public func averageLine(chartData : T, + markerName : String = "Average", + 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, + lineColour : lineColour, + strokeStyle : strokeStyle, + isAverage : true)) + } +} From e8e75650dda35a1b951823e07d416d9dca9af94f Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 27 Jan 2021 19:51:47 +0000 Subject: [PATCH 009/152] Tidy Up --- Sources/SwiftUICharts/LineChart/Views/LineChartView.swift | 4 ++-- Sources/SwiftUICharts/Shared/Models/Protocols.swift | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index 6fb279e9..39c0c368 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -21,9 +21,9 @@ public struct LineChart: View where ChartData: LineChartData { self.minValue = DataFunctions.minValue(dataPoints: chartData.dataSets.dataPoints) self.range = DataFunctions.range(dataPoints: chartData.dataSets.dataPoints) - setupLegends() +// setupLegends() } - + public var body: some View { // if chartData.isGreaterThanTwo { diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols.swift index 61496c3f..d443bdd9 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols.swift @@ -7,6 +7,13 @@ import SwiftUI +/* + ToDo + + setupLegend causes crash on second init --- Maybe ass to protocol + + */ + public protocol ChartData: ObservableObject, Identifiable { associatedtype Set : DataSet associatedtype DataPoint: ChartDataPoint From f436eca91ff04267949f861ec271aff265829081 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 28 Jan 2021 08:49:43 +0000 Subject: [PATCH 010/152] Move legend functions into protocol. --- .../BarChart/Models/BarChartData.swift | 72 ++++++ .../BarChart/Models/MultiBarChartData.swift | 76 ++++++- .../BarChart/Views/BarChartView.swift | 6 +- .../BarChart/Views/GroupedBarChart.swift | 6 +- .../LineChart/Models/LineChartData.swift | 34 +++ .../LineChart/Models/MultiLineChartData.swift | 36 +++ .../LineChart/Views/FilledLineChart.swift | 4 +- .../LineChart/Views/LineChartView.swift | 22 +- .../LineChart/Views/MultiLineChart.swift | 7 +- .../PieChart/Models/PieChartData.swift | 32 +++ .../PieChart/Views/PieChart.swift | 8 +- .../Shared/Extras/AddLegends.swift | 148 ++++++------- .../Shared/Models/LegendData.swift | 4 +- .../Shared/Models/Protocols.swift | 9 +- .../Shared/Views/LegendView.swift | 209 +++++++++++------- 15 files changed, 469 insertions(+), 204 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index 36b560b6..1bff38e8 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -114,6 +114,78 @@ public class BarChartData: LineAndBarChartData { DataFunctions.dataSetAverage(from: dataSets) } + public func setupLegends() { + switch self.dataSets.style.colourFrom { + case .barStyle: + if dataSets.style.colourType == .colour, + let colour = dataSets.style.colour + { + self.legends.append(LegendData(legend : dataSets.legendTitle, + colour : colour, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if dataSets.style.colourType == .gradientColour, + let colours = dataSets.style.colours + { + self.legends.append(LegendData(legend : dataSets.legendTitle, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if dataSets.style.colourType == .gradientStops, + let stops = dataSets.style.stops + { + self.legends.append(LegendData(legend : dataSets.legendTitle, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } + case .dataPoints: + + for data in dataSets.dataPoints { + + if data.colourType == .colour, + let colour = data.colour, + let legend = data.pointDescription + { + self.legends.append(LegendData(legend : legend, + colour : colour, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if data.colourType == .gradientColour, + let colours = data.colours, + let legend = data.pointDescription + { + self.legends.append(LegendData(legend : legend, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if data.colourType == .gradientStops, + let stops = data.stops, + let legend = data.pointDescription + { + self.legends.append(LegendData(legend : legend, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } + } + } + } + public typealias Set = BarDataSet public typealias DataPoint = BarChartDataPoint } diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift index 5defa469..f34f4cb1 100644 --- a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift @@ -49,10 +49,6 @@ public class MultiBarChartData: LineAndBarChartData { self.viewData = ChartViewData() self.chartType = (chartType: .bar, dataSetType: .multi) } - - public func legendOrder() -> [LegendData] { - return [LegendData]() - } public func getHeaderLocation() -> InfoBoxPlacement { return self.chartStyle.infoBoxPlacement @@ -125,6 +121,78 @@ public class MultiBarChartData: LineAndBarChartData { DataFunctions.multiDataSetAverage(from: dataSets) } + public func setupLegends() { + switch dataSets.dataSets[0].style.colourFrom { + case .barStyle: + if dataSets.dataSets[0].style.colourType == .colour, + let colour = dataSets.dataSets[0].style.colour + { + self.legends.append(LegendData(legend : dataSets.dataSets[0].legendTitle, + colour : colour, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if dataSets.dataSets[0].style.colourType == .gradientColour, + let colours = dataSets.dataSets[0].style.colours + { + self.legends.append(LegendData(legend : dataSets.dataSets[0].legendTitle, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if dataSets.dataSets[0].style.colourType == .gradientStops, + let stops = dataSets.dataSets[0].style.stops + { + self.legends.append(LegendData(legend : dataSets.dataSets[0].legendTitle, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } + case .dataPoints: + + for data in dataSets.dataSets[0].dataPoints { + + if data.colourType == .colour, + let colour = data.colour, + let legend = data.pointDescription + { + self.legends.append(LegendData(legend : legend, + colour : colour, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if data.colourType == .gradientColour, + let colours = data.colours, + let legend = data.pointDescription + { + self.legends.append(LegendData(legend : legend, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if data.colourType == .gradientStops, + let stops = data.stops, + let legend = data.pointDescription + { + self.legends.append(LegendData(legend : legend, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } + } + } + } + public typealias Set = MultiBarDataSet public typealias DataPoint = BarChartDataPoint } diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift b/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift index 949d987d..357a28bd 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift @@ -16,9 +16,8 @@ public struct BarChart: View where ChartData: BarChartData { public init(chartData: ChartData) { self.chartData = chartData self.maxValue = DataFunctions.maxValue(dataPoints: chartData.dataSets.dataPoints) -// chartData.viewData.chartType = .bar - setupLegends() + chartData.setupLegends() } public var body: some View { @@ -47,7 +46,4 @@ public struct BarChart: View where ChartData: BarChartData { } } } - internal mutating func setupLegends() { - AddLegends.setupBar(chartData: &chartData, dataSet: chartData.dataSets) - } } diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index 4ea830a4..450d4832 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -16,9 +16,8 @@ public struct GroupedBarChart: View where ChartData: MultiBarChartDat public init(chartData: ChartData) { self.chartData = chartData self.maxValue = DataFunctions.multiDataSetMaxValue(from: chartData.dataSets) -// chartData.viewData.chartType = .bar -// setupLegends() + chartData.setupLegends() } public var body: some View { @@ -52,7 +51,4 @@ public struct GroupedBarChart: View where ChartData: MultiBarChartDat } } } -// internal mutating func setupLegends() { -// Legends.setupBar(chartData: &chartData, dataSet: chartData.dataSets) -// } } diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index 20751200..16f246db 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -131,6 +131,40 @@ public class LineChartData: LineAndBarChartData, LineChartProtocol { DataFunctions.dataSetAverage(from: dataSets) } + public func setupLegends() { + if dataSets.style.colourType == .colour, + let colour = dataSets.style.colour + { + self.legends.append(LegendData(legend : dataSets.legendTitle, + colour : colour, + strokeStyle: dataSets.style.strokeStyle, + prioity : 1, + chartType : .line)) + + } else if dataSets.style.colourType == .gradientColour, + let colours = dataSets.style.colours + { + self.legends.append(LegendData(legend : dataSets.legendTitle, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: dataSets.style.strokeStyle, + prioity : 1, + chartType : .line)) + + } else if dataSets.style.colourType == .gradientStops, + let stops = dataSets.style.stops + { + self.legends.append(LegendData(legend : dataSets.legendTitle, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: dataSets.style.strokeStyle, + prioity : 1, + chartType : .line)) + } + } + public typealias Set = LineDataSet public typealias DataPoint = LineChartDataPoint } diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift index 61459ca9..96038a2f 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -136,6 +136,42 @@ public class MultiLineChartData: LineAndBarChartData, LineChartProtocol { DataFunctions.multiDataSetAverage(from: dataSets) } + public func setupLegends() { + for dataSet in dataSets.dataSets { + if dataSet.style.colourType == .colour, + let colour = dataSet.style.colour + { + self.legends.append(LegendData(legend : dataSet.legendTitle, + colour : colour, + strokeStyle: dataSet.style.strokeStyle, + prioity : 1, + chartType : .line)) + + } else if dataSet.style.colourType == .gradientColour, + let colours = dataSet.style.colours + { + self.legends.append(LegendData(legend : dataSet.legendTitle, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: dataSet.style.strokeStyle, + prioity : 1, + chartType : .line)) + + } else if dataSet.style.colourType == .gradientStops, + let stops = dataSet.style.stops + { + self.legends.append(LegendData(legend : dataSet.legendTitle, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: dataSet.style.strokeStyle, + prioity : 1, + chartType : .line)) + } + } + } + public typealias Set = MultiLineDataSet public typealias DataPoint = LineChartDataPoint } diff --git a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift index 88236e11..72b25dc1 100644 --- a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift @@ -20,6 +20,7 @@ public struct FilledLineChart: View where ChartData: LineChartData { self.chartData = chartData self.minValue = DataFunctions.minValue(dataPoints: chartData.dataSets.dataPoints) self.range = DataFunctions.range(dataPoints: chartData.dataSets.dataPoints) + chartData.setupLegends() } public var body: some View { @@ -52,7 +53,4 @@ public struct FilledLineChart: View where ChartData: LineChartData { } // } else { CustomNoDataView(chartData: chartData) } } - internal mutating func setupLegends() { - AddLegends.setupLine(chartData: &chartData, dataSet: chartData.dataSets) - } } diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index 39c0c368..4f957df8 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -21,7 +21,7 @@ public struct LineChart: View where ChartData: LineChartData { self.minValue = DataFunctions.minValue(dataPoints: chartData.dataSets.dataPoints) self.range = DataFunctions.range(dataPoints: chartData.dataSets.dataPoints) -// setupLegends() + chartData.setupLegends() } public var body: some View { @@ -75,24 +75,4 @@ public struct LineChart: View where ChartData: LineChartData { } // } else { CustomNoDataView(chartData: chartData) } } - internal mutating func setupLegends() { - AddLegends.setupLine(chartData: &chartData, dataSet: chartData.dataSets) - } } - - -//internal struct LineShapeModifiers: ViewModifier { -// private let chartData : T -// -// internal init(_ chartData : T) { -// self.chartData = chartData -// } -// -// func body(content: Content) -> some View { -// content -// .background(Color(.gray).opacity(0.01)) -// .if(chartData.viewData.hasXAxisLabels) { $0.xAxisBorder(chartData: chartData) } -// .if(chartData.viewData.hasYAxisLabels) { $0.yAxisBorder(chartData: chartData) } -// } -//} -// diff --git a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift index 963ed30f..fe659609 100644 --- a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift @@ -21,7 +21,7 @@ public struct MultiLineChart: View where ChartData: MultiLineChartDat self.minValue = DataFunctions.multiDataSetMinValue(from: chartData.dataSets) self.range = DataFunctions.multiDataSetRange(from: chartData.dataSets) - setupLegends() + chartData.setupLegends() } public var body: some View { @@ -59,9 +59,4 @@ public struct MultiLineChart: View where ChartData: MultiLineChartDat } // } else { CustomNoDataView(chartData: chartData) } } - internal mutating func setupLegends() { - for dataSet in chartData.dataSets.dataSets { - AddLegends.setupLine(chartData: &chartData, dataSet: dataSet) - } - } } diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift index 9d36cf02..f81647e1 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift @@ -50,6 +50,38 @@ public class PieChartData: PieChartDataProtocol { return [HashablePoint(x: 0, y: 0)] } + public func setupLegends() { + if dataSets.style.colourType == .colour, + let colour = dataSets.style.colour + { + self.legends.append(LegendData(legend : dataSets.legendTitle, + colour : colour, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if dataSets.style.colourType == .gradientColour, + let colours = dataSets.style.colours + { + self.legends.append(LegendData(legend : dataSets.legendTitle, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if dataSets.style.colourType == .gradientStops, + let stops = dataSets.style.stops + { + self.legends.append(LegendData(legend : dataSets.legendTitle, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } + } + public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint } diff --git a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift index 1611615b..ed91ca19 100644 --- a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift @@ -7,16 +7,16 @@ import SwiftUI -public struct PieChart: View { +public struct PieChart: View where ChartData: PieChartData { - let chartData : PieChartData + @ObservedObject var chartData : ChartData let pieSegments : [PieSegmentShape] let strokeWidth : Double? @State var startAnimation : Bool = false - public init(chartData : PieChartData, + public init(chartData : ChartData, strokeWidth: Double? = nil ) { self.chartData = chartData @@ -34,6 +34,8 @@ public struct PieChart: View { startAngle += amount } pieSegments = segments + + chartData.setupLegends() } @ViewBuilder diff --git a/Sources/SwiftUICharts/Shared/Extras/AddLegends.swift b/Sources/SwiftUICharts/Shared/Extras/AddLegends.swift index 4a774a86..da6ff37c 100644 --- a/Sources/SwiftUICharts/Shared/Extras/AddLegends.swift +++ b/Sources/SwiftUICharts/Shared/Extras/AddLegends.swift @@ -7,77 +7,77 @@ import SwiftUI -internal struct AddLegends { - static func setupLine(chartData: inout T, dataSet: LineDataSet) { - if dataSet.style.colourType == .colour, - let colour = dataSet.style.colour - { - chartData.legends.append(LegendData(legend : dataSet.legendTitle, - colour : colour, - strokeStyle: dataSet.style.strokeStyle, - prioity : 1, - chartType : .line)) - - } else if dataSet.style.colourType == .gradientColour, - let colours = dataSet.style.colours - { - chartData.legends.append(LegendData(legend : dataSet.legendTitle, - colours : colours, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: dataSet.style.strokeStyle, - prioity : 1, - chartType : .line)) - - } else if dataSet.style.colourType == .gradientStops, - let stops = dataSet.style.stops - { - chartData.legends.append(LegendData(legend : dataSet.legendTitle, - stops : stops, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: dataSet.style.strokeStyle, - prioity : 1, - chartType : .line)) - } -// chartData.viewData.chartType = .line - } - - static func setupBar(chartData: inout T, dataSet: BarDataSet) { - - switch chartData.dataSets.style.colourFrom { - case .barStyle: - if dataSet.style.colourType == .colour, - let colour = dataSet.style.colour - { - chartData.legends.append(LegendData(legend : dataSet.legendTitle, - colour : colour, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if dataSet.style.colourType == .gradientColour, - let colours = dataSet.style.colours - { - chartData.legends.append(LegendData(legend : dataSet.legendTitle, - colours : colours, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if dataSet.style.colourType == .gradientStops, - let stops = dataSet.style.stops - { - chartData.legends.append(LegendData(legend : dataSet.legendTitle, - stops : stops, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } - case .dataPoints: - Text("") - } - } -} +//internal struct AddLegends { +// static func setupLine(chartData: inout T, dataSet: LineDataSet) { +// if dataSet.style.colourType == .colour, +// let colour = dataSet.style.colour +// { +// chartData.legends.append(LegendData(legend : dataSet.legendTitle, +// colour : colour, +// strokeStyle: dataSet.style.strokeStyle, +// prioity : 1, +// chartType : .line)) +// +// } else if dataSet.style.colourType == .gradientColour, +// let colours = dataSet.style.colours +// { +// chartData.legends.append(LegendData(legend : dataSet.legendTitle, +// colours : colours, +// startPoint : .leading, +// endPoint : .trailing, +// strokeStyle: dataSet.style.strokeStyle, +// prioity : 1, +// chartType : .line)) +// +// } else if dataSet.style.colourType == .gradientStops, +// let stops = dataSet.style.stops +// { +// chartData.legends.append(LegendData(legend : dataSet.legendTitle, +// stops : stops, +// startPoint : .leading, +// endPoint : .trailing, +// strokeStyle: dataSet.style.strokeStyle, +// prioity : 1, +// chartType : .line)) +// } +//// chartData.viewData.chartType = .line +// } +// +// static func setupBar(chartData: inout T, dataSet: BarDataSet) { +// +// switch chartData.dataSets.style.colourFrom { +// case .barStyle: +// if dataSet.style.colourType == .colour, +// let colour = dataSet.style.colour +// { +// chartData.legends.append(LegendData(legend : dataSet.legendTitle, +// colour : colour, +// strokeStyle: nil, +// prioity : 1, +// chartType : .bar)) +// } else if dataSet.style.colourType == .gradientColour, +// let colours = dataSet.style.colours +// { +// chartData.legends.append(LegendData(legend : dataSet.legendTitle, +// colours : colours, +// startPoint : .leading, +// endPoint : .trailing, +// strokeStyle: nil, +// prioity : 1, +// chartType : .bar)) +// } else if dataSet.style.colourType == .gradientStops, +// let stops = dataSet.style.stops +// { +// chartData.legends.append(LegendData(legend : dataSet.legendTitle, +// stops : stops, +// startPoint : .leading, +// endPoint : .trailing, +// strokeStyle: nil, +// prioity : 1, +// chartType : .bar)) +// } +// case .dataPoints: +// Text("") +// } +// } +//} diff --git a/Sources/SwiftUICharts/Shared/Models/LegendData.swift b/Sources/SwiftUICharts/Shared/Models/LegendData.swift index 1f279478..b5963e73 100644 --- a/Sources/SwiftUICharts/Shared/Models/LegendData.swift +++ b/Sources/SwiftUICharts/Shared/Models/LegendData.swift @@ -8,7 +8,9 @@ import SwiftUI /// Data model for Legends -public struct LegendData: CTColourStyle, Hashable { +public struct LegendData: CTColourStyle, Hashable, Identifiable { + + public var id: UUID = UUID() var chartType : ChartType public var colourType : ColourType diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols.swift index d443bdd9..21fa2f9a 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols.swift @@ -7,13 +7,6 @@ import SwiftUI -/* - ToDo - - setupLegend causes crash on second init --- Maybe ass to protocol - - */ - public protocol ChartData: ObservableObject, Identifiable { associatedtype Set : DataSet associatedtype DataPoint: ChartDataPoint @@ -41,7 +34,7 @@ public protocol ChartData: ObservableObject, Identifiable { /// - chartSize: The size of the chart view as the parent view. func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] - + func setupLegends() -> Void } extension ChartData { /// Sets the order the Legends are layed out in. diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index f2db767a..2550cfe0 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -30,84 +30,145 @@ internal struct LegendView: View where T: ChartData { switch legend.chartType { 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) - } - } - } + + line(legend) + case .bar: - Text("Hello") -// 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) -// } -// } + + bar(legend) + case .pie: - Text("") + + pie(legend) + } } } } + + func line(_ legend: LegendData) -> some View { + Group { + 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) + } + } + } + } + } +} + +func bar(_ legend: LegendData) -> some View { + Group { + 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) + } + } + } +} + +func pie(_ legend: LegendData) -> some View { + Group { + if let colour = legend.colour { + HStack { + Circle() + .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 { + Circle() + .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) + } + } + } } From 39c105c03f39f25bd6a3fb8f47ee0a5d4d52aab0 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sat, 30 Jan 2021 06:35:39 +0000 Subject: [PATCH 011/152] Tidy up --- .../BarChart/Models/BarChartData.swift | 1 + .../PieChart/Shapes/PieSegmentShape.swift | 11 +- .../PieChart/Views/PieChart.swift | 44 +++-- .../Shared/Models/ChartViewData.swift | 5 +- .../Shared/Models/Protocols.swift | 11 +- .../Shared/ViewModifiers/Legends.swift | 5 + .../Shared/ViewModifiers/TouchOverlay.swift | 4 +- .../Shared/Views/LegendView.swift | 161 +++++++++--------- 8 files changed, 116 insertions(+), 126 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index 1bff38e8..53bcf231 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -188,5 +188,6 @@ public class BarChartData: LineAndBarChartData { public typealias Set = BarDataSet public typealias DataPoint = BarChartDataPoint + } diff --git a/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift b/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift index f104f2ef..b7f02512 100644 --- a/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift +++ b/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift @@ -9,19 +9,10 @@ import SwiftUI internal struct PieSegmentShape: Shape, Identifiable { - let data : PieChartDataPoint - var id : UUID { data.id } + var id : UUID var startAngle : Double var amount : Double - var animatableData: AnimatablePair { - get { AnimatablePair(startAngle, amount) } - set { - startAngle = newValue.first - amount = newValue.second - } - } - internal func path(in rect: CGRect) -> Path { let radius = min(rect.width, rect.height) / 2 diff --git a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift index ed91ca19..0ae209ec 100644 --- a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift @@ -9,33 +9,18 @@ import SwiftUI public struct PieChart: View where ChartData: PieChartData { - @ObservedObject var chartData : ChartData + @ObservedObject var chartData : ChartData - let pieSegments : [PieSegmentShape] let strokeWidth : Double? @State var startAnimation : Bool = false - + public init(chartData : ChartData, strokeWidth: Double? = nil ) { self.chartData = chartData self.strokeWidth = strokeWidth - - var segments = [PieSegmentShape]() - let total = chartData.dataSets.dataPoints.reduce(0) { $0 + $1.value } - var startAngle = -Double.pi / 2 - - for data in chartData.dataSets.dataPoints { - let amount = .pi * 2 * (data.value / total) - let segment = PieSegmentShape(data: data, startAngle: startAngle, amount: amount) - segments.append(segment) - startAngle += amount - } - pieSegments = segments - - chartData.setupLegends() } @ViewBuilder @@ -49,15 +34,24 @@ public struct PieChart: View where ChartData: PieChartData { } public var body: some View { - ZStack { - ForEach(pieSegments) { segment in - segment - .fill(segment.data.colour) + GeometryReader { geo in + 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) + .scaleEffect(chartData.viewData.isTouchCurrent ? 1 : 10) + .opacity(startAnimation ? 1 : 0) + .animation(Animation.spring().delay(Double(data) * 0.06)) + .if(chartData.viewData.isTouchCurrent) { $0.scaleEffect(2) } + } + } + .mask(mask) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true } - } - .mask(mask) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true } } } diff --git a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift index 9ad9ef65..4bfebcde 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift @@ -9,10 +9,7 @@ import Foundation /// Data model to pass view information internally so the layout can configure its self. public struct ChartViewData { - - /// Pass the type of chart being used to view modifiers. -// var chartTypchartData.chartTypee : ChartType = .line - + /// If the chart has labels on the X axis, the Y axis needs a different layout var hasXAxisLabels : Bool = false diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols.swift index 21fa2f9a..e68b19ef 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols.swift @@ -19,6 +19,9 @@ public protocol ChartData: ObservableObject, Identifiable { var viewData : ChartViewData { get set } var noDataText : Text { get set } var chartType : (chartType: ChartType, dataSetType: DataSetType) { get } + + // Sets the order the Legends are layed out in. + /// - Returns: Ordered array of Legends. func legendOrder() -> [LegendData] func getHeaderLocation() -> InfoBoxPlacement @@ -34,11 +37,11 @@ public protocol ChartData: ObservableObject, Identifiable { /// - chartSize: The size of the chart view as the parent view. func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] - func setupLegends() -> Void + func setupLegends() /*-> [LegendData]*/ } + + extension ChartData { - /// Sets the order the Legends are layed out in. - /// - Returns: Ordered array of Legends. public func legendOrder() -> [LegendData] { return legends.sorted { $0.prioity < $1.prioity} } @@ -109,8 +112,6 @@ public protocol CTColourStyle { } - - public protocol ChartDataPoint: Hashable, Identifiable { var id : ID { get } var value : Double { get set } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift index 26e6b4eb..5cad9e9f 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift @@ -15,9 +15,13 @@ internal struct Legends: ViewModifier where T: ChartData { VStack { content LegendView(chartData: chartData) + } } } + + + extension View { /// Displays legends under the chart. /// - Returns: Legends from the charts data and any markers. @@ -25,3 +29,4 @@ extension View { self.modifier(Legends(chartData: chartData)) } } + diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 5394447a..c43efec3 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -56,16 +56,16 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { self.selectedPoints = chartData.getDataPoint(touchLocation: touchLocation, chartSize: geo) + self.pointLocations = chartData.getPointLocation(touchLocation: touchLocation, chartSize: geo) - if chartData.getHeaderLocation() == .floating { setBoxLocationation(boxFrame: boxFrame, chartSize: geo) markerLocation.x = setMarkerXLocation(chartSize: geo) markerLocation.y = setMarkerYLocation(chartSize: geo) - + } else if chartData.getHeaderLocation() == .header { chartData.viewData.isTouchCurrent = true diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index 2550cfe0..f8d6cdd6 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -10,7 +10,7 @@ import SwiftUI internal struct LegendView: View where T: ChartData { @ObservedObject var chartData : T - + internal init(chartData: T) { self.chartData = chartData } @@ -25,6 +25,7 @@ internal struct LegendView: View where T: ChartData { internal var body: some View { LazyVGrid(columns: columns, alignment: .leading) { + ForEach(chartData.legendOrder(), id: \.self) { legend in switch legend.chartType { @@ -86,88 +87,88 @@ internal struct LegendView: View where T: ChartData { } } } -} - -func bar(_ legend: LegendData) -> some View { - Group { - 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) + + func bar(_ legend: LegendData) -> some View { + Group { + 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) + } } } } -} - -func pie(_ legend: LegendData) -> some View { - Group { - if let colour = legend.colour { - HStack { - Circle() - .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 { - Circle() - .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) + + func pie(_ legend: LegendData) -> some View { + Group { + if let colour = legend.colour { + HStack { + Circle() + .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 { + Circle() + .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 { + Circle() + .fill(LinearGradient(gradient: Gradient(stops: stops), + startPoint: startPoint, + endPoint: endPoint)) + .frame(width: 20, height: 20) + Text(legend.legend) + .font(.caption) + } } } } From daf7251c595b30cc4504de7e04c09476966e1742 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sat, 30 Jan 2021 06:36:16 +0000 Subject: [PATCH 012/152] Add touch functionality. --- .../PieChart/Models/PieChartData.swift | 121 ++++++++++++------ 1 file changed, 82 insertions(+), 39 deletions(-) diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift index f81647e1..ad30fd24 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift @@ -9,14 +9,14 @@ import SwiftUI public class PieChartData: PieChartDataProtocol { - @Published public var id: UUID = UUID() - @Published public var dataSets: PieDataSet - @Published public var metadata: ChartMetadata? - @Published public var xAxisLabels: [String]? - @Published public var chartStyle: PieChartStyle - @Published public var legends: [LegendData] - @Published public var viewData: ChartViewData - + @Published public var id : UUID = UUID() + @Published public var dataSets : PieDataSet + @Published public var metadata : ChartMetadata? + @Published public var xAxisLabels : [String]? + @Published public var chartStyle : PieChartStyle + @Published public var legends : [LegendData] + @Published public var viewData : ChartViewData + public var noDataText: Text public var chartType: (chartType: ChartType, dataSetType: DataSetType) @@ -34,54 +34,94 @@ public class PieChartData: PieChartDataProtocol { self.viewData = ChartViewData() self.noDataText = noDataText self.chartType = (chartType: .pie, dataSetType: .single) + + self.setupLegends() + + self.makeDataPoints() + } + + 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 + } } public func getHeaderLocation() -> InfoBoxPlacement { return self.chartStyle.infoBoxPlacement } - - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [PieChartDataPoint] { - let points : [PieChartDataPoint] = [] + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [PieChartDataPoint] { + var points : [PieChartDataPoint] = [] + let touchDegree = degree(from: touchLocation, in: chartSize.frame(in: .local)) + 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 { + points.append(data) + } return points } public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { + + return [HashablePoint(x: 0, y: 0)] } public func setupLegends() { - if dataSets.style.colourType == .colour, - let colour = dataSets.style.colour - { - self.legends.append(LegendData(legend : dataSets.legendTitle, - colour : colour, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if dataSets.style.colourType == .gradientColour, - let colours = dataSets.style.colours - { - self.legends.append(LegendData(legend : dataSets.legendTitle, - colours : colours, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if dataSets.style.colourType == .gradientStops, - let stops = dataSets.style.stops - { - self.legends.append(LegendData(legend : dataSets.legendTitle, - stops : stops, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) + for data in dataSets.dataPoints { + if let legend = data.pointDescription { + self.legends.append(LegendData(legend : legend, + colour : data.colour, + strokeStyle: nil, + prioity : 1, + chartType : .pie)) + } } } + + func degree(from touchLocation: CGPoint, in rect: CGRect) -> CGFloat { + + // http://www.cplusplus.com/reference/cmath/atan2/ + // https://stackoverflow.com/a/25398191 + + 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 + } + + + /* + // Where 0 is north + let degrees = atan2(-x, -y) * CGFloat(180 / Double.pi) + if (degrees > 0) { + return 360 - degrees + } else { + return 0 - degrees + } + + Where 0 is East + var degrees = atan2(y, x) * CGFloat(180 / Double.pi) + if (degrees < 0) { + degrees = 360 + degrees + } + return degrees + */ + } + public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint } @@ -96,6 +136,9 @@ public struct PieChartDataPoint: ChartDataPoint { public var colour : Color + var startAngle : Double = 0 + var amount : Double = 0 + public init(value : Double, xAxisLabel : String? = nil, pointDescription: String? = nil, From 107c6388acd24d96596b5817a79989373c188eae Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sat, 30 Jan 2021 17:52:51 +0000 Subject: [PATCH 013/152] Spilt Info data away from view data. --- .../BarChart/Models/BarChartData.swift | 22 ++++--- .../BarChart/Models/MultiBarChartData.swift | 22 ++++--- .../LineChart/Models/LineChartData.swift | 25 +++++--- .../LineChart/Models/MultiLineChartData.swift | 23 +++++--- .../PieChart/Models/PieChartData.swift | 17 +++--- .../Shared/Models/ChartViewData.swift | 8 ++- .../Shared/Models/Protocols.swift | 5 +- .../Shared/ViewModifiers/HeaderBox.swift | 57 ++++++++++--------- .../Shared/ViewModifiers/TouchOverlay.swift | 13 ++--- 9 files changed, 115 insertions(+), 77 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index 53bcf231..83a2164b 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -16,7 +16,9 @@ public class BarChartData: LineAndBarChartData { @Published public var xAxisLabels : [String]? @Published public var chartStyle : BarChartStyle @Published public var legends : [LegendData] - @Published public var viewData : ChartViewData + @Published public var viewData : ChartViewData + @Published public var infoView : InfoViewData = InfoViewData() + public var noDataText : Text = Text("No Data") public var chartType : (chartType: ChartType, dataSetType: DataSetType) @@ -120,7 +122,8 @@ public class BarChartData: LineAndBarChartData { if dataSets.style.colourType == .colour, let colour = dataSets.style.colour { - self.legends.append(LegendData(legend : dataSets.legendTitle, + self.legends.append(LegendData(id : dataSets.id, + legend : dataSets.legendTitle, colour : colour, strokeStyle: nil, prioity : 1, @@ -128,7 +131,8 @@ public class BarChartData: LineAndBarChartData { } else if dataSets.style.colourType == .gradientColour, let colours = dataSets.style.colours { - self.legends.append(LegendData(legend : dataSets.legendTitle, + self.legends.append(LegendData(id : dataSets.id, + legend : dataSets.legendTitle, colours : colours, startPoint : .leading, endPoint : .trailing, @@ -138,7 +142,8 @@ public class BarChartData: LineAndBarChartData { } else if dataSets.style.colourType == .gradientStops, let stops = dataSets.style.stops { - self.legends.append(LegendData(legend : dataSets.legendTitle, + self.legends.append(LegendData(id : dataSets.id, + legend : dataSets.legendTitle, stops : stops, startPoint : .leading, endPoint : .trailing, @@ -154,7 +159,8 @@ public class BarChartData: LineAndBarChartData { let colour = data.colour, let legend = data.pointDescription { - self.legends.append(LegendData(legend : legend, + self.legends.append(LegendData(id : data.id, + legend : legend, colour : colour, strokeStyle: nil, prioity : 1, @@ -163,7 +169,8 @@ public class BarChartData: LineAndBarChartData { let colours = data.colours, let legend = data.pointDescription { - self.legends.append(LegendData(legend : legend, + self.legends.append(LegendData(id : data.id, + legend : legend, colours : colours, startPoint : .leading, endPoint : .trailing, @@ -174,7 +181,8 @@ public class BarChartData: LineAndBarChartData { let stops = data.stops, let legend = data.pointDescription { - self.legends.append(LegendData(legend : legend, + self.legends.append(LegendData(id : data.id, + legend : legend, stops : stops, startPoint : .leading, endPoint : .trailing, diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift index f34f4cb1..3abba51c 100644 --- a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift @@ -16,7 +16,9 @@ public class MultiBarChartData: LineAndBarChartData { @Published public var xAxisLabels : [String]? @Published public var chartStyle : BarChartStyle @Published public var legends : [LegendData] - @Published public var viewData : ChartViewData + @Published public var viewData : ChartViewData + @Published public var infoView : InfoViewData = InfoViewData() + public var noDataText : Text = Text("No Data") public var chartType : (chartType: ChartType, dataSetType: DataSetType) @@ -127,7 +129,8 @@ public class MultiBarChartData: LineAndBarChartData { if dataSets.dataSets[0].style.colourType == .colour, let colour = dataSets.dataSets[0].style.colour { - self.legends.append(LegendData(legend : dataSets.dataSets[0].legendTitle, + self.legends.append(LegendData(id : dataSets.dataSets[0].id, + legend : dataSets.dataSets[0].legendTitle, colour : colour, strokeStyle: nil, prioity : 1, @@ -135,7 +138,8 @@ public class MultiBarChartData: LineAndBarChartData { } else if dataSets.dataSets[0].style.colourType == .gradientColour, let colours = dataSets.dataSets[0].style.colours { - self.legends.append(LegendData(legend : dataSets.dataSets[0].legendTitle, + self.legends.append(LegendData(id : dataSets.dataSets[0].id, + legend : dataSets.dataSets[0].legendTitle, colours : colours, startPoint : .leading, endPoint : .trailing, @@ -145,7 +149,8 @@ public class MultiBarChartData: LineAndBarChartData { } else if dataSets.dataSets[0].style.colourType == .gradientStops, let stops = dataSets.dataSets[0].style.stops { - self.legends.append(LegendData(legend : dataSets.dataSets[0].legendTitle, + self.legends.append(LegendData(id : dataSets.dataSets[0].id, + legend : dataSets.dataSets[0].legendTitle, stops : stops, startPoint : .leading, endPoint : .trailing, @@ -161,7 +166,8 @@ public class MultiBarChartData: LineAndBarChartData { let colour = data.colour, let legend = data.pointDescription { - self.legends.append(LegendData(legend : legend, + self.legends.append(LegendData(id : data.id, + legend : legend, colour : colour, strokeStyle: nil, prioity : 1, @@ -170,7 +176,8 @@ public class MultiBarChartData: LineAndBarChartData { let colours = data.colours, let legend = data.pointDescription { - self.legends.append(LegendData(legend : legend, + self.legends.append(LegendData(id : data.id, + legend : legend, colours : colours, startPoint : .leading, endPoint : .trailing, @@ -181,7 +188,8 @@ public class MultiBarChartData: LineAndBarChartData { let stops = data.stops, let legend = data.pointDescription { - self.legends.append(LegendData(legend : legend, + self.legends.append(LegendData(id : data.id, + legend : legend, stops : stops, startPoint : .leading, endPoint : .trailing, diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index 16f246db..105ecd58 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -28,7 +28,9 @@ public class LineChartData: LineAndBarChartData, LineChartProtocol { @Published public var legends : [LegendData] /// Data model to hold data about the Views layout. - @Published public var viewData : ChartViewData + @Published public var viewData : ChartViewData + + @Published public var infoView : InfoViewData = InfoViewData() public var noDataText : Text = Text("No Data") @@ -37,7 +39,7 @@ public class LineChartData: LineAndBarChartData, LineChartProtocol { public init(dataSets : LineDataSet, metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, - chartStyle : LineChartStyle = LineChartStyle(), + chartStyle : LineChartStyle = LineChartStyle(), calculations: CalculationType = .none ) { self.dataSets = dataSets @@ -93,10 +95,12 @@ public class LineChartData: LineAndBarChartData, LineChartProtocol { public func getXAxidLabels() -> some View { HStack(spacing: 0) { ForEach(dataSets.dataPoints) { data in - Text(data.xAxisLabel ?? "") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) + if let label = data.xAxisLabel { + Text(label) + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + } if data != self.dataSets.dataPoints[self.dataSets.dataPoints.count - 1] { Spacer() .frame(minWidth: 0, maxWidth: 500) @@ -135,7 +139,8 @@ public class LineChartData: LineAndBarChartData, LineChartProtocol { if dataSets.style.colourType == .colour, let colour = dataSets.style.colour { - self.legends.append(LegendData(legend : dataSets.legendTitle, + self.legends.append(LegendData(id : dataSets.id, + legend : dataSets.legendTitle, colour : colour, strokeStyle: dataSets.style.strokeStyle, prioity : 1, @@ -144,7 +149,8 @@ public class LineChartData: LineAndBarChartData, LineChartProtocol { } else if dataSets.style.colourType == .gradientColour, let colours = dataSets.style.colours { - self.legends.append(LegendData(legend : dataSets.legendTitle, + self.legends.append(LegendData(id : dataSets.id, + legend : dataSets.legendTitle, colours : colours, startPoint : .leading, endPoint : .trailing, @@ -155,7 +161,8 @@ public class LineChartData: LineAndBarChartData, LineChartProtocol { } else if dataSets.style.colourType == .gradientStops, let stops = dataSets.style.stops { - self.legends.append(LegendData(legend : dataSets.legendTitle, + self.legends.append(LegendData(id : dataSets.id, + legend : dataSets.legendTitle, stops : stops, startPoint : .leading, endPoint : .trailing, diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift index 96038a2f..555ed158 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -28,7 +28,9 @@ public class MultiLineChartData: LineAndBarChartData, LineChartProtocol { @Published public var legends : [LegendData] /// Data model to hold data about the Views layout. - @Published public var viewData : ChartViewData + @Published public var viewData : ChartViewData + + @Published public var infoView : InfoViewData = InfoViewData() public var noDataText : Text = Text("No Data") @@ -98,10 +100,12 @@ public class MultiLineChartData: LineAndBarChartData, LineChartProtocol { public func getXAxidLabels() -> some View { HStack(spacing: 0) { ForEach(dataSets.dataSets[0].dataPoints) { data in - Text(data.xAxisLabel ?? "") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) + if let label = data.xAxisLabel { + Text(label) + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + } if data != self.dataSets.dataSets[0].dataPoints[self.dataSets.dataSets[0].dataPoints.count - 1] { Spacer() .frame(minWidth: 0, maxWidth: 500) @@ -141,7 +145,8 @@ public class MultiLineChartData: LineAndBarChartData, LineChartProtocol { if dataSet.style.colourType == .colour, let colour = dataSet.style.colour { - self.legends.append(LegendData(legend : dataSet.legendTitle, + self.legends.append(LegendData(id : dataSets.id, + legend : dataSet.legendTitle, colour : colour, strokeStyle: dataSet.style.strokeStyle, prioity : 1, @@ -150,7 +155,8 @@ public class MultiLineChartData: LineAndBarChartData, LineChartProtocol { } else if dataSet.style.colourType == .gradientColour, let colours = dataSet.style.colours { - self.legends.append(LegendData(legend : dataSet.legendTitle, + self.legends.append(LegendData(id : dataSets.id, + legend : dataSet.legendTitle, colours : colours, startPoint : .leading, endPoint : .trailing, @@ -161,7 +167,8 @@ public class MultiLineChartData: LineAndBarChartData, LineChartProtocol { } else if dataSet.style.colourType == .gradientStops, let stops = dataSet.style.stops { - self.legends.append(LegendData(legend : dataSet.legendTitle, + self.legends.append(LegendData(id : dataSets.id, + legend : dataSet.legendTitle, stops : stops, startPoint : .leading, endPoint : .trailing, diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift index ad30fd24..f56c63e6 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift @@ -15,8 +15,8 @@ public class PieChartData: PieChartDataProtocol { @Published public var xAxisLabels : [String]? @Published public var chartStyle : PieChartStyle @Published public var legends : [LegendData] - @Published public var viewData : ChartViewData - + @Published public var infoView : InfoViewData + public var noDataText: Text public var chartType: (chartType: ChartType, dataSetType: DataSetType) @@ -31,7 +31,7 @@ public class PieChartData: PieChartDataProtocol { self.xAxisLabels = xAxisLabels self.chartStyle = chartStyle self.legends = [LegendData]() - self.viewData = ChartViewData() + self.infoView = InfoViewData() self.noDataText = noDataText self.chartType = (chartType: .pie, dataSetType: .single) @@ -43,11 +43,14 @@ public class PieChartData: PieChartDataProtocol { 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 + + print(startAngle, amount) + startAngle += amount } } @@ -68,14 +71,14 @@ public class PieChartData: PieChartDataProtocol { public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - - return [HashablePoint(x: 0, y: 0)] + return [HashablePoint(x: touchLocation.x, y: touchLocation.y)] } public func setupLegends() { for data in dataSets.dataPoints { if let legend = data.pointDescription { - self.legends.append(LegendData(legend : legend, + self.legends.append(LegendData(id : data.id, + legend : legend, colour : data.colour, strokeStyle: nil, prioity : 1, diff --git a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift index 4bfebcde..8bc4956a 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift @@ -8,7 +8,7 @@ import Foundation /// Data model to pass view information internally so the layout can configure its self. -public struct ChartViewData { +public struct ChartViewData { /// If the chart has labels on the X axis, the Y axis needs a different layout var hasXAxisLabels : Bool = false @@ -16,6 +16,10 @@ public struct ChartViewData { /// If the chart has labels on the Y axis, the X axis needs a different layout var hasYAxisLabels : Bool = false +} + +/// Data model to pass view information internally so the layout can configure its self. +public struct InfoViewData { /** Is there currently input (touch or click) on the chart @@ -31,7 +35,7 @@ public struct ChartViewData { Used by TitleBox */ - var touchOverlayInfo : [D] = [] + var touchOverlayInfo : [DP] = [] /** Set specifier of data point readout diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols.swift index e68b19ef..07457aef 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols.swift @@ -16,7 +16,7 @@ public protocol ChartData: ObservableObject, Identifiable { var metadata : ChartMetadata? { get set } var xAxisLabels : [String]? { get set } // Not pie var legends : [LegendData] { get set } - var viewData : ChartViewData { get set } + var infoView : InfoViewData { get set } var noDataText : Text { get set } var chartType : (chartType: ChartType, dataSetType: DataSetType) { get } @@ -37,7 +37,7 @@ public protocol ChartData: ObservableObject, Identifiable { /// - chartSize: The size of the chart view as the parent view. func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] - func setupLegends() /*-> [LegendData]*/ + func setupLegends() } @@ -52,6 +52,7 @@ public protocol LineAndBarChartData : ChartData { associatedtype Body : View var chartStyle : Style { get set } + var viewData : ChartViewData { get set } func getXAxidLabels() -> Body func getYLabels() -> [Double] diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index 58d7a501..e83872d7 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -44,9 +44,9 @@ internal struct HeaderBox: ViewModifier where T: ChartData { var touchOverlay: some View { VStack(alignment: .trailing) { - if chartData.viewData.isTouchCurrent { - ForEach(chartData.viewData.touchOverlayInfo, id: \.self) { info in - Text("\(info.value, specifier: chartData.viewData.touchSpecifier)") + if chartData.infoView.isTouchCurrent { + ForEach(chartData.infoView.touchOverlayInfo, id: \.self) { info in + Text("\(info.value, specifier: chartData.infoView.touchSpecifier)") .font(.title3) Text("\(info.pointDescription ?? "")") .font(.subheadline) @@ -60,40 +60,43 @@ internal struct HeaderBox: ViewModifier where T: ChartData { } } - @ViewBuilder + internal func body(content: Content) -> some View { // if chartData.isGreaterThanTwo { - #if !os(tvOS) - if chartData.getHeaderLocation() == .floating { - VStack(alignment: .leading) { - titleBox - content - } - } else if chartData.getHeaderLocation() == .header { - VStack(alignment: .leading) { - HStack(spacing: 0) { - HStack(spacing: 0) { - titleBox - Spacer() - } - .frame(minWidth: 0, maxWidth: .infinity) - Spacer() + Group { + #if !os(tvOS) + if chartData.getHeaderLocation() == .floating { + VStack(alignment: .leading) { + titleBox + content + } + } else if chartData.getHeaderLocation() == .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 } + } + + #elseif os(tvOS) + VStack(alignment: .leading) { + titleBox content } + #endif } - #elseif os(tvOS) - VStack(alignment: .leading) { - titleBox - content - } - #endif // } else { content } } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index c43efec3..475704b3 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -17,8 +17,6 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { private let specifier : String private let touchMarkerLineWidth : CGFloat = 1 // API? - /// Boolean that indicates whether touch is currently being detected - @State private var isTouchCurrent : Bool = false /// Current location of the touch input @State private var touchLocation : CGPoint = CGPoint(x: 0, y: 0) /// The data point closest to the touch input @@ -52,7 +50,6 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { DragGesture(minimumDistance: 0) .onChanged { (value) in touchLocation = value.location - isTouchCurrent = true self.selectedPoints = chartData.getDataPoint(touchLocation: touchLocation, chartSize: geo) @@ -68,16 +65,16 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { } else if chartData.getHeaderLocation() == .header { - chartData.viewData.isTouchCurrent = true - chartData.viewData.touchOverlayInfo = selectedPoints + chartData.infoView.isTouchCurrent = true + chartData.infoView.touchOverlayInfo = selectedPoints } } .onEnded { _ in - isTouchCurrent = false - chartData.viewData.isTouchCurrent = false + chartData.infoView.isTouchCurrent = false + chartData.infoView.touchOverlayInfo = [] } ) - if isTouchCurrent { + if chartData.infoView.isTouchCurrent { ForEach(pointLocations, id: \.self) { location in TouchOverlayMarker(position: location) .stroke(Color(.gray), lineWidth: touchMarkerLineWidth) From cf9192650e4d2eb5b8a4a32cd58a8f732689f29e Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sat, 30 Jan 2021 17:54:12 +0000 Subject: [PATCH 014/152] Fix second load animations. --- .../BarChart/Views/SubViews/Bars.swift | 18 +++++++++++++++--- .../Views/SubViews/LineChartSubViews.swift | 9 +++++++++ .../Shared/Extras/Extensions.swift | 12 ++++++++++-- .../Shared/ViewModifiers/XAxisGrid.swift | 5 ++++- .../Shared/ViewModifiers/YAxisGrid.swift | 5 ++++- 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift index de9075d6..d7a3b4a8 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift @@ -36,7 +36,13 @@ struct ColourBar: View { .scaleEffect(y: startAnimation ? CGFloat(data.value / maxValue) : 0, anchor: .bottom) .scaleEffect(x: style.barWidth, anchor: .center) .animateOnAppear(using: chartStyle.globalAnimation) { - self.startAnimation.toggle() + self.startAnimation = true + } + .animateOnAppear(using: chartStyle.globalAnimation) { + self.startAnimation = true + } + .animateOnDisappear(using: chartStyle.globalAnimation) { + self.startAnimation = false } } } @@ -78,7 +84,10 @@ struct GradientColoursBar: View { .scaleEffect(y: startAnimation ? CGFloat(data.value / maxValue) : 0, anchor: .bottom) .scaleEffect(x: style.barWidth, anchor: .center) .animateOnAppear(using: chartStyle.globalAnimation) { - self.startAnimation.toggle() + self.startAnimation = true + } + .animateOnDisappear(using: chartStyle.globalAnimation) { + self.startAnimation = false } } } @@ -120,7 +129,10 @@ struct GradientStopsBar: View { .scaleEffect(y: startAnimation ? CGFloat(data.value / maxValue) : 0, anchor: .bottom) .scaleEffect(x: style.barWidth, anchor: .center) .animateOnAppear(using: chartStyle.globalAnimation) { - self.startAnimation.toggle() + self.startAnimation = true + } + .animateOnDisappear(using: chartStyle.globalAnimation) { + self.startAnimation = false } } } diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift index 93b0f330..a585f078 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift @@ -37,6 +37,9 @@ struct LineChartColourSubView: View where CD: LineAndBarChartData { .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true } + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = false + } } } @@ -81,6 +84,9 @@ struct LineChartColoursSubView: View where CD: LineAndBarChartData { .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true } + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = false + } } } @@ -124,6 +130,9 @@ struct LineChartStopsSubView: View where CD: LineAndBarChartData { .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true } + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = false + } } } diff --git a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift index e7274f75..e13eb63f 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift @@ -18,8 +18,8 @@ extension View { extension View { @ViewBuilder func `ifElse`(_ condition: Bool, - if ifTransform: (Self) -> TrueContent, - else elseTransform: (Self) -> FalseContent + if ifTransform: (Self) -> TrueContent, + else elseTransform: (Self) -> FalseContent ) -> some View { if condition { @@ -40,4 +40,12 @@ extension View { } } } + + func animateOnDisappear(using animation: Animation = Animation.easeInOut(duration: 1), _ action: @escaping () -> Void) -> some View { + return onDisappear { + withAnimation(animation) { + action() + } + } + } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift index 11a0a00c..777e8c02 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift @@ -55,7 +55,10 @@ internal struct VerticalGridView: View where T: LineAndBarChartData { dashPhase: chartData.chartStyle.xAxisGridStyle.dashPhase)) .frame(width: chartData.chartStyle.xAxisGridStyle.lineWidth) .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation.toggle() + self.startAnimation = true + } + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = false } } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift index 942cfa6b..86aaef67 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift @@ -59,7 +59,10 @@ internal struct HorizontalGridView: View where T: LineAndBarChartData { dashPhase: chartData.chartStyle.yAxisGridStyle.dashPhase)) .frame(height: chartData.chartStyle.yAxisGridStyle.lineWidth) .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation.toggle() + self.startAnimation = true + } + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = false } } } From 3149d083c9177d357d0ec8b137e37bb87c1f4d9f Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sat, 30 Jan 2021 17:55:04 +0000 Subject: [PATCH 015/152] Start to add interaction animations. --- .../PieChart/Views/PieChart.swift | 40 +++++++++++-------- .../Shared/Models/LegendData.swift | 14 +++++-- .../Shared/ViewModifiers/YAxisPOI.swift | 3 +- .../Shared/Views/LegendView.swift | 9 ++--- 4 files changed, 40 insertions(+), 26 deletions(-) diff --git a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift index 0ae209ec..9595dbcc 100644 --- a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift @@ -9,32 +9,33 @@ import SwiftUI public struct PieChart: View where ChartData: PieChartData { - @ObservedObject var chartData : ChartData + @ObservedObject var chartData: ChartData let strokeWidth : Double? @State var startAnimation : Bool = false - public init(chartData : ChartData, + public init(chartData: ChartData, strokeWidth: Double? = nil ) { self.chartData = chartData self.strokeWidth = strokeWidth } - - @ViewBuilder - var mask: some View { - if let strokeWidth = strokeWidth { - Circle() - .strokeBorder(Color(.white), lineWidth: CGFloat(strokeWidth)) - } else { - Circle() - } - } +// +// @ViewBuilder +// var mask: some View { +// if let strokeWidth = strokeWidth { +// Circle() +// .strokeBorder(Color(.white), lineWidth: CGFloat(strokeWidth)) +// } else { +// Circle() +// } +// } public var body: some View { GeometryReader { geo in + ZStack { ForEach(chartData.dataSets.dataPoints.indices, id: \.self) { data in PieSegmentShape(id: chartData.dataSets.dataPoints[data].id, @@ -42,16 +43,23 @@ public struct PieChart: View where ChartData: PieChartData { amount: chartData.dataSets.dataPoints[data].amount) .fill(chartData.dataSets.dataPoints[data].colour) .scaleEffect(startAnimation ? 1 : 0) - .scaleEffect(chartData.viewData.isTouchCurrent ? 1 : 10) .opacity(startAnimation ? 1 : 0) .animation(Animation.spring().delay(Double(data) * 0.06)) - .if(chartData.viewData.isTouchCurrent) { $0.scaleEffect(2) } + .if(chartData.infoView.touchOverlayInfo == [chartData.dataSets.dataPoints[data]]) { + $0 + .scaleEffect(1.1) + .zIndex(1) + .shadow(color: Color.primary, radius: 10) + } } } - .mask(mask) + } +// .mask(mask) .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true } - } + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = false + } } } diff --git a/Sources/SwiftUICharts/Shared/Models/LegendData.swift b/Sources/SwiftUICharts/Shared/Models/LegendData.swift index b5963e73..809f3ed7 100644 --- a/Sources/SwiftUICharts/Shared/Models/LegendData.swift +++ b/Sources/SwiftUICharts/Shared/Models/LegendData.swift @@ -10,7 +10,7 @@ import SwiftUI /// Data model for Legends public struct LegendData: CTColourStyle, Hashable, Identifiable { - public var id: UUID = UUID() + public var id : UUID var chartType : ChartType public var colourType : ColourType @@ -41,12 +41,14 @@ public struct LegendData: CTColourStyle, Hashable, Identifiable { /// - colour: Single Colour /// - strokeStyle: Stroke Style /// - prioity: Used to make sure the charts data legend is first - public init(legend : String, + public init(id : UUID, + legend : String, colour : Color, strokeStyle: Stroke?, prioity : Int, chartType : ChartType ) { + self.id = id self.legend = legend self.colour = colour self.colours = nil @@ -67,7 +69,8 @@ public struct LegendData: CTColourStyle, Hashable, Identifiable { /// - endPoint: End point for Gradient /// - strokeStyle: Stroke Style /// - prioity: Used to make sure the charts data legend is first - public init(legend : String, + public init(id : UUID, + legend : String, colours : [Color], startPoint : UnitPoint, endPoint : UnitPoint, @@ -75,6 +78,7 @@ public struct LegendData: CTColourStyle, Hashable, Identifiable { prioity : Int, chartType : ChartType ) { + self.id = id self.legend = legend self.colour = nil self.colours = colours @@ -95,7 +99,8 @@ public struct LegendData: CTColourStyle, Hashable, Identifiable { /// - endPoint: End point for Gradient /// - strokeStyle: Stroke Style /// - prioity: Used to make sure the charts data legend is first - public init(legend : String, + public init(id : UUID, + legend : String, stops : [GradientStop], startPoint : UnitPoint, endPoint : UnitPoint, @@ -103,6 +108,7 @@ public struct LegendData: CTColourStyle, Hashable, Identifiable { prioity : Int, chartType : ChartType ) { + self.id = id self.legend = legend self.colour = nil self.colours = nil diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift index 5df064dd..81936ef0 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift @@ -52,7 +52,8 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { .stroke(lineColour, style: strokeStyle) .onAppear { if !chartData.legends.contains(where: { $0.legend == markerName }) { // init twice - chartData.legends.append(LegendData(legend : markerName, + chartData.legends.append(LegendData(id : UUID(), + legend : markerName, colour : lineColour, strokeStyle : Stroke.strokeStyleToStroke(strokeStyle: strokeStyle), prioity : 2, diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index f8d6cdd6..2f7e051d 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -25,7 +25,6 @@ internal struct LegendView: View where T: ChartData { internal var body: some View { LazyVGrid(columns: columns, alignment: .leading) { - ForEach(chartData.legendOrder(), id: \.self) { legend in switch legend.chartType { @@ -33,15 +32,15 @@ internal struct LegendView: View where T: ChartData { case .line: line(legend) - + case .bar: bar(legend) - + case .pie: - + pie(legend) - + .if(chartData.infoView.isTouchCurrent && legend.id == chartData.infoView.touchOverlayInfo[0].id as! UUID) { $0.scaleEffect(1.2, anchor: .leading) } } } } From 2fd4d784bf96aa975b6210e3d12622206344dd2d Mon Sep 17 00:00:00 2001 From: Will Dale Date: Mon, 1 Feb 2021 09:22:39 +0000 Subject: [PATCH 016/152] Tidy up and rename protocols. --- .../BarChart/Models/BarChartData.swift | 33 ++--- .../BarChart/Models/BarChartDataPoint.swift | 2 +- .../BarChart/Models/BarChartStyle.swift | 2 +- .../BarChart/Models/MultiBarChartData.swift | 29 +---- .../BarChart/Views/BarChartView.swift | 11 +- .../BarChart/Views/GroupedBarChart.swift | 11 +- .../LineChart/Models/LineChartData.swift | 102 ++++++++-------- .../LineChart/Models/LineChartDataPoint.swift | 2 +- .../LineChart/Models/LineChartStyle.swift | 13 +- .../LineChart/Models/MultiLineChartData.swift | 51 +++----- .../LineChart/Shapes/LineShape.swift | 12 +- .../LineChart/Views/FilledLineChart.swift | 37 +++++- .../LineChart/Views/LineChartView.swift | 60 ++++----- .../LineChart/Views/MultiLineChart.swift | 40 ++++-- .../Views/SubViews/LineChartSubViews.swift | 57 ++++++--- .../PieChart/Models/PieChartData.swift | 100 --------------- .../PieChart/Models/PieChartDataPoint.swift | 35 ++++++ .../PieChart/Models/PieChartStyle.swift | 35 ------ .../PieChart/Models/PieDataSet.swift | 31 +++++ .../PieChart/Models/PieSegmentStyle.swift | 32 +++++ .../PieChart/Views/DoughnutChart.swift | 60 +++++++++ .../PieChart/Views/PieChart.swift | 30 ++--- .../Shared/Extras/DataFunctions.swift | 45 ++----- .../SwiftUICharts/Shared/Extras/Enums.swift | 4 + .../Shared/Models/ChartViewData.swift | 2 +- .../Shared/Models/LegendData.swift | 21 ++-- .../Shared/Models/Protocols.swift | 114 +++++++++++++++--- .../SwiftUICharts/Shared/Models/Stroke.swift | 3 +- .../Shared/Shapes/PointShape.swift | 5 +- .../Shared/ViewModifiers/Legends.swift | 3 - .../Shared/ViewModifiers/PointMarkers.swift | 30 +++-- .../Shared/ViewModifiers/TouchOverlay.swift | 5 +- .../Shared/ViewModifiers/YAxisLabels.swift | 1 - .../Shared/Views/LegendView.swift | 8 +- .../Shared/Views/TouchOverlayBox.swift | 4 +- 35 files changed, 551 insertions(+), 479 deletions(-) create mode 100644 Sources/SwiftUICharts/PieChart/Models/PieChartDataPoint.swift create mode 100644 Sources/SwiftUICharts/PieChart/Models/PieDataSet.swift create mode 100644 Sources/SwiftUICharts/PieChart/Models/PieSegmentStyle.swift create mode 100644 Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index 83a2164b..8241726a 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -7,7 +7,7 @@ import SwiftUI -public class BarChartData: LineAndBarChartData { +public class BarChartData: BarChartDataProtocol { public let id : UUID = UUID() @@ -17,7 +17,7 @@ public class BarChartData: LineAndBarChartData { @Published public var chartStyle : BarChartStyle @Published public var legends : [LegendData] @Published public var viewData : ChartViewData - @Published public var infoView : InfoViewData = InfoViewData() + @Published public var infoView : InfoViewData = InfoViewData() public var noDataText : Text = Text("No Data") public var chartType : (chartType: ChartType, dataSetType: DataSetType) @@ -35,6 +35,7 @@ public class BarChartData: LineAndBarChartData { self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (.bar, .single) + self.setupLegends() } public init(dataSets : BarDataSet, @@ -50,6 +51,7 @@ public class BarChartData: LineAndBarChartData { self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (chartType: .bar, dataSetType: .single) + self.setupLegends() } public func getHeaderLocation() -> InfoBoxPlacement { @@ -70,7 +72,7 @@ public class BarChartData: LineAndBarChartData { var locations : [HashablePoint] = [] let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count) - let ySection : CGFloat = chartSize.size.height / CGFloat(DataFunctions.dataSetMaxValue(from: dataSets)) + let ySection : CGFloat = chartSize.size.height / CGFloat(self.getMaxValue()) let index : Int = Int((touchLocation.x) / xSection) if index >= 0 && index < dataSets.dataPoints.count { @@ -79,7 +81,7 @@ public class BarChartData: LineAndBarChartData { } return locations } - + public func getXAxidLabels() -> some View { HStack(spacing: 0) { ForEach(dataSets.dataPoints) { data in @@ -94,28 +96,7 @@ public class BarChartData: LineAndBarChartData { } } } - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let maxValue: Double = DataFunctions.dataSetMaxValue(from: dataSets) - for index in 0...self.chartStyle.yAxisNumberOfLabels { - labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) - } - return labels - } - - public func getRange() -> Double { - DataFunctions.dataSetRange(from: dataSets) - } - public func getMinValue() -> Double { - DataFunctions.dataSetMinValue(from: dataSets) - } - public func getMaxValue() -> Double { - DataFunctions.dataSetMaxValue(from: dataSets) - } - public func getAverage() -> Double { - DataFunctions.dataSetAverage(from: dataSets) - } - + public func setupLegends() { switch self.dataSets.style.colourFrom { case .barStyle: diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift index ef24f489..34f71184 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift @@ -8,7 +8,7 @@ import SwiftUI /// Data model for a data point. -public struct BarChartDataPoint: ChartDataPoint, CTColourStyle { +public struct BarChartDataPoint: CTLineAndBarDataPoint, CTColourStyle { public let id = UUID() diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift index 3fdbeeb3..21fa6a4e 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift @@ -7,7 +7,7 @@ import SwiftUI -public struct BarChartStyle: CTLineAndBarChartStyle { +public struct BarChartStyle: CTBarChartStyle { /// Placement of the information box that appears on touch input. public var infoBoxPlacement : InfoBoxPlacement diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift index 3abba51c..28ae0e87 100644 --- a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift @@ -7,7 +7,7 @@ import SwiftUI -public class MultiBarChartData: LineAndBarChartData { +public class MultiBarChartData: BarChartDataProtocol { public let id : UUID = UUID() @@ -35,6 +35,7 @@ public class MultiBarChartData: LineAndBarChartData { self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (chartType: .bar, dataSetType: .multi) + self.setupLegends() } public init(dataSets : MultiBarDataSet, @@ -50,6 +51,7 @@ public class MultiBarChartData: LineAndBarChartData { self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (chartType: .bar, dataSetType: .multi) + self.setupLegends() } public func getHeaderLocation() -> InfoBoxPlacement { @@ -71,7 +73,7 @@ public class MultiBarChartData: LineAndBarChartData { var locations : [HashablePoint] = [] for dataSet in dataSets.dataSets { let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count) - let ySection : CGFloat = chartSize.size.height / CGFloat(DataFunctions.multiDataSetMaxValue(from: dataSets)) + let ySection : CGFloat = chartSize.size.height / CGFloat(getMaxValue()) let index = Int((touchLocation.x) / xSection) @@ -82,7 +84,7 @@ public class MultiBarChartData: LineAndBarChartData { } return locations } - public func getXAxidLabels() -> some View { + public func getXAxidLabels() -> some View { HStack(spacing: 100) { ForEach(dataSets.dataSets) { dataSet in HStack(spacing: 0) { @@ -101,27 +103,6 @@ public class MultiBarChartData: LineAndBarChartData { } .padding(.horizontal, -4) } - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let maxValue : Double = DataFunctions.multiDataSetMaxValue(from: dataSets) - for index in 0...self.chartStyle.yAxisNumberOfLabels { - labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) - } - return labels - } - - public func getRange() -> Double { - DataFunctions.multiDataSetRange(from: dataSets) - } - public func getMinValue() -> Double { - DataFunctions.multiDataSetMinValue(from: dataSets) - } - public func getMaxValue() -> Double { - DataFunctions.multiDataSetMaxValue(from: dataSets) - } - public func getAverage() -> Double { - DataFunctions.multiDataSetAverage(from: dataSets) - } public func setupLegends() { switch dataSets.dataSets[0].style.colourFrom { diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift b/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift index 357a28bd..49406332 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift @@ -10,14 +10,9 @@ import SwiftUI public struct BarChart: View where ChartData: BarChartData { @ObservedObject var chartData: ChartData - - let maxValue : Double - + public init(chartData: ChartData) { self.chartData = chartData - self.maxValue = DataFunctions.maxValue(dataPoints: chartData.dataSets.dataPoints) - - chartData.setupLegends() } public var body: some View { @@ -32,7 +27,7 @@ public struct BarChart: View where ChartData: BarChartData { dataPoint: dataPoint, style: chartData.dataSets.style, chartStyle: chartData.chartStyle, - maxValue: maxValue) + maxValue: chartData.getMaxValue()) case .dataPoints: @@ -40,7 +35,7 @@ public struct BarChart: View where ChartData: BarChartData { dataPoint : dataPoint, style : chartData.dataSets.style, chartStyle : chartData.chartStyle, - maxValue : maxValue) + maxValue : chartData.getMaxValue()) } } diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index 450d4832..95252167 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -10,14 +10,9 @@ import SwiftUI public struct GroupedBarChart: View where ChartData: MultiBarChartData { @ObservedObject var chartData: ChartData - - let maxValue : Double - + public init(chartData: ChartData) { self.chartData = chartData - self.maxValue = DataFunctions.multiDataSetMaxValue(from: chartData.dataSets) - - chartData.setupLegends() } public var body: some View { @@ -34,7 +29,7 @@ public struct GroupedBarChart: View where ChartData: MultiBarChartDat dataPoint: dataPoint, style: dataSet.style, chartStyle: chartData.chartStyle, - maxValue: maxValue) + maxValue: chartData.getMaxValue()) case .dataPoints: @@ -42,7 +37,7 @@ public struct GroupedBarChart: View where ChartData: MultiBarChartDat dataPoint: dataPoint, style: dataSet.style, chartStyle: chartData.chartStyle, - maxValue: maxValue) + maxValue: chartData.getMaxValue()) } } diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index 105ecd58..d3a701f5 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -8,25 +8,20 @@ import SwiftUI /// The central model from which the chart is drawn. -public class LineChartData: LineAndBarChartData, LineChartProtocol { +public class LineChartData: LineChartDataProtocol { public let id : UUID = UUID() /// Data model containing the datapoints: Value, Label, Description and Date. Individual colouring for bar chart. @Published public var dataSets : LineDataSet - /// 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 : LineChartStyle - /// Array of data to populate the chart legend. @Published public var legends : [LegendData] - /// Data model to hold data about the Views layout. @Published public var viewData : ChartViewData @@ -49,6 +44,8 @@ public class LineChartData: LineAndBarChartData, LineChartProtocol { self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (chartType: .line, dataSetType: .single) + + self.setupLegends() } public init(dataSets : LineDataSet, @@ -64,12 +61,15 @@ public class LineChartData: LineAndBarChartData, LineChartProtocol { self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (chartType: .line, dataSetType: .single) + + self.setupLegends() } public func getHeaderLocation() -> InfoBoxPlacement { return self.chartStyle.infoBoxPlacement } + // MARK: Touch public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [LineChartDataPoint] { var points : [LineChartDataPoint] = [] let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count - 1) @@ -83,59 +83,31 @@ public class LineChartData: LineAndBarChartData, LineChartProtocol { public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { var locations : [HashablePoint] = [] + let minValue : Double + let range : Double + + switch self.chartStyle.baseline { + case .minimumValue: + minValue = self.getMinValue() + range = self.getRange() + case .zero: + minValue = 0 + range = self.getMaxValue() + } + + let ySection : CGFloat = chartSize.size.height / CGFloat(range) let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count - 1) - let ySection : CGFloat = chartSize.size.height / CGFloat(DataFunctions.dataSetRange(from: dataSets)) + let index : Int = Int((touchLocation.x + (xSection / 2)) / xSection) if index >= 0 && index < dataSets.dataPoints.count { locations.append(HashablePoint(x: CGFloat(index) * xSection, - y: (CGFloat(dataSets.dataPoints[index].value - DataFunctions.dataSetMinValue(from: dataSets)) * -ySection) + chartSize.size.height)) + y: (CGFloat(dataSets.dataPoints[index].value - minValue) * -ySection) + chartSize.size.height)) } return locations } - public func getXAxidLabels() -> some View { - HStack(spacing: 0) { - ForEach(dataSets.dataPoints) { data in - if let label = data.xAxisLabel { - Text(label) - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) - } - if data != self.dataSets.dataPoints[self.dataSets.dataPoints.count - 1] { - Spacer() - .frame(minWidth: 0, maxWidth: 500) - } - } - } - .padding(.horizontal, -4) - } - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let dataRange : Double = DataFunctions.dataSetRange(from: dataSets) - let minValue : Double = DataFunctions.dataSetMinValue(from: dataSets) - - let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) - labels.append(minValue) - for index in 1...self.chartStyle.yAxisNumberOfLabels { - labels.append(minValue + range * Double(index)) - } - return labels - } - - public func getRange() -> Double { - DataFunctions.dataSetRange(from: dataSets) - } - public func getMinValue() -> Double { - DataFunctions.dataSetMinValue(from: dataSets) - } - public func getMaxValue() -> Double { - DataFunctions.dataSetMaxValue(from: dataSets) - } - public func getAverage() -> Double { - DataFunctions.dataSetAverage(from: dataSets) - } - + // MARK: Legends public func setupLegends() { + if dataSets.style.colourType == .colour, let colour = dataSets.style.colour { @@ -145,7 +117,7 @@ public class LineChartData: LineAndBarChartData, LineChartProtocol { strokeStyle: dataSets.style.strokeStyle, prioity : 1, chartType : .line)) - + } else if dataSets.style.colourType == .gradientColour, let colours = dataSets.style.colours { @@ -157,7 +129,7 @@ public class LineChartData: LineAndBarChartData, LineChartProtocol { strokeStyle: dataSets.style.strokeStyle, prioity : 1, chartType : .line)) - + } else if dataSets.style.colourType == .gradientStops, let stops = dataSets.style.stops { @@ -175,3 +147,27 @@ public class LineChartData: LineAndBarChartData, LineChartProtocol { public typealias Set = LineDataSet public typealias DataPoint = LineChartDataPoint } + +//MARK: - LineAndBarChartData + +extension LineChartData { + + // MARK: Labels + public func getXAxidLabels() -> some View { + HStack(spacing: 0) { + ForEach(dataSets.dataPoints) { data in + if let label = data.xAxisLabel { + Text(label) + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + } + if data != self.dataSets.dataPoints[self.dataSets.dataPoints.count - 1] { + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } + .padding(.horizontal, -4) + } +} diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift index e65fc893..ec02588d 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift @@ -8,7 +8,7 @@ import SwiftUI /// Data model for a data point. -public struct LineChartDataPoint: ChartDataPoint { +public struct LineChartDataPoint: CTLineAndBarDataPoint { public let id = UUID() diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift index 33a5f764..f249f734 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift @@ -8,7 +8,7 @@ import SwiftUI /// Model for controlling the overall aesthetic of the chart. -public struct LineChartStyle: CTLineAndBarChartStyle { +public struct LineChartStyle: CTLineChartStyle { /// Placement of the information box that appears on touch input. public var infoBoxPlacement : InfoBoxPlacement @@ -28,6 +28,8 @@ public struct LineChartStyle: CTLineAndBarChartStyle { /// Number Of Labels on Y Axis public var yAxisNumberOfLabels : Int + public var baseline : Baseline + /// Gobal control of animations. public var globalAnimation : Animation @@ -40,6 +42,7 @@ public struct LineChartStyle: CTLineAndBarChartStyle { /// - 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 + /// - baseline: Whether the chart is drawn from baseline of zero or the minimum datapoint value. /// - globalAnimation: Gobal control of animations. public init(infoBoxPlacement : InfoBoxPlacement = .floating, xAxisGridStyle : GridStyle = GridStyle(), @@ -48,6 +51,7 @@ public struct LineChartStyle: CTLineAndBarChartStyle { xAxisLabelsFrom : LabelsFrom = .dataPoint, yAxisLabelPosition : YAxisLabelPosistion = .leading, yAxisNumberOfLabels : Int = 10, + baseline : Baseline = .minimumValue, globalAnimation : Animation = Animation.linear(duration: 1) ) { self.infoBoxPlacement = infoBoxPlacement @@ -59,6 +63,13 @@ public struct LineChartStyle: CTLineAndBarChartStyle { self.yAxisLabelPosition = yAxisLabelPosition self.yAxisNumberOfLabels = yAxisNumberOfLabels + self.baseline = baseline + self.globalAnimation = globalAnimation } } + +public enum Baseline { + case minimumValue + case zero +} diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift index 555ed158..17decd66 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -8,7 +8,7 @@ import SwiftUI /// The central model from which the chart is drawn. -public class MultiLineChartData: LineAndBarChartData, LineChartProtocol { +public class MultiLineChartData: LineChartDataProtocol { public let id : UUID = UUID() @@ -49,6 +49,7 @@ public class MultiLineChartData: LineAndBarChartData, LineChartProtocol { self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (.line, .multi) + self.setupLegends() } public init(dataSets : MultiLineDataSet, @@ -64,6 +65,7 @@ public class MultiLineChartData: LineAndBarChartData, LineChartProtocol { self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (chartType: .line, dataSetType: .multi) + self.setupLegends() } public func getHeaderLocation() -> InfoBoxPlacement { @@ -87,12 +89,24 @@ public class MultiLineChartData: LineAndBarChartData, LineChartProtocol { var locations : [HashablePoint] = [] for dataSet in dataSets.dataSets { + let minValue : Double + let range : Double + + switch self.chartStyle.baseline { + case .minimumValue: + minValue = self.getMinValue() + range = self.getRange() + case .zero: + minValue = 0 + range = self.getMaxValue() + } + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) - let ySection : CGFloat = chartSize.size.height / CGFloat(DataFunctions.multiDataSetRange(from: dataSets)) + let ySection : CGFloat = chartSize.size.height / CGFloat(range) let index : Int = Int((touchLocation.x + (xSection / 2)) / xSection) if index >= 0 && index < dataSet.dataPoints.count { locations.append(HashablePoint(x: CGFloat(index) * xSection, - y: (CGFloat(dataSet.dataPoints[index].value - DataFunctions.multiDataSetMinValue(from: dataSets)) * -ySection) + chartSize.size.height)) + y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.size.height)) } } return locations @@ -114,38 +128,13 @@ public class MultiLineChartData: LineAndBarChartData, LineChartProtocol { } .padding(.horizontal, -4) } - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let dataRange : Double = DataFunctions.multiDataSetRange(from: dataSets) - let minValue : Double = DataFunctions.multiDataSetMinValue(from: dataSets) - - let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) - labels.append(minValue) - for index in 1...self.chartStyle.yAxisNumberOfLabels { - labels.append(minValue + range * Double(index)) - } - return labels - } - - public func getRange() -> Double { - DataFunctions.multiDataSetRange(from: dataSets) - } - public func getMinValue() -> Double { - DataFunctions.multiDataSetMinValue(from: dataSets) - } - public func getMaxValue() -> Double { - DataFunctions.multiDataSetMaxValue(from: dataSets) - } - public func getAverage() -> Double { - DataFunctions.multiDataSetAverage(from: dataSets) - } public func setupLegends() { for dataSet in dataSets.dataSets { if dataSet.style.colourType == .colour, let colour = dataSet.style.colour { - self.legends.append(LegendData(id : dataSets.id, + self.legends.append(LegendData(id : dataSet.id, legend : dataSet.legendTitle, colour : colour, strokeStyle: dataSet.style.strokeStyle, @@ -155,7 +144,7 @@ public class MultiLineChartData: LineAndBarChartData, LineChartProtocol { } else if dataSet.style.colourType == .gradientColour, let colours = dataSet.style.colours { - self.legends.append(LegendData(id : dataSets.id, + self.legends.append(LegendData(id : dataSet.id, legend : dataSet.legendTitle, colours : colours, startPoint : .leading, @@ -167,7 +156,7 @@ public class MultiLineChartData: LineAndBarChartData, LineChartProtocol { } else if dataSet.style.colourType == .gradientStops, let stops = dataSet.style.stops { - self.legends.append(LegendData(id : dataSets.id, + self.legends.append(LegendData(id : dataSet.id, legend : dataSet.legendTitle, stops : stops, startPoint : .leading, diff --git a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift index 7abcfbea..25b2f570 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift @@ -9,7 +9,7 @@ import SwiftUI internal struct LineShape: Shape { - private let dataSet : LineDataSet + private let dataPoints : [LineChartDataPoint] /// Drawing style of the line private let lineType : LineType /// If it's to be filled some extra lines need to be drawn @@ -18,13 +18,13 @@ internal struct LineShape: Shape { private let minValue : Double private let range : Double - internal init(dataSet : LineDataSet, + internal init(dataPoints: [LineChartDataPoint], lineType : LineType, isFilled : Bool, minValue : Double, range : Double ) { - self.dataSet = dataSet + self.dataPoints = dataPoints self.lineType = lineType self.isFilled = isFilled self.minValue = minValue @@ -33,14 +33,14 @@ internal struct LineShape: Shape { internal func path(in rect: CGRect) -> Path { - let x : CGFloat = rect.width / CGFloat(dataSet.dataPoints.count - 1) + let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1) let y : CGFloat = rect.height / CGFloat(range) switch lineType { case .curvedLine: - return curvedLine(rect, x, y, dataSet.dataPoints, minValue, range, isFilled) + return curvedLine(rect, x, y, dataPoints, minValue, range, isFilled) case .line: - return straightLine(rect, x, y, dataSet.dataPoints, minValue, range, isFilled) + return straightLine(rect, x, y, dataPoints, minValue, range, isFilled) } } diff --git a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift index 72b25dc1..89a34b68 100644 --- a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift @@ -18,9 +18,15 @@ public struct FilledLineChart: View where ChartData: LineChartData { public init(chartData: ChartData) { self.chartData = chartData - self.minValue = DataFunctions.minValue(dataPoints: chartData.dataSets.dataPoints) - self.range = DataFunctions.range(dataPoints: chartData.dataSets.dataPoints) - chartData.setupLegends() + + switch chartData.chartStyle.baseline { + case .minimumValue: + self.minValue = chartData.getMinValue() + self.range = chartData.getRange() + case .zero: + self.minValue = 0 + self.range = chartData.getMaxValue() + } } public var body: some View { @@ -31,7 +37,12 @@ public struct FilledLineChart: View where ChartData: LineChartData { let colour = chartData.dataSets.style.colour { - LineChartColourSubView(chartData: chartData, dataSet: chartData.dataSets, style: chartData.dataSets.style, minValue: minValue, range: range, colour: colour, isFilled: true) + LineChartColourSubView(chartData: chartData, + dataSet: chartData.dataSets, + minValue: minValue, + range: range, + colour: colour, + isFilled: true) } else if chartData.dataSets.style.colourType == .gradientColour, let colours = chartData.dataSets.style.colours, @@ -39,7 +50,14 @@ public struct FilledLineChart: View where ChartData: LineChartData { let endPoint = chartData.dataSets.style.endPoint { - LineChartColoursSubView(chartData: chartData, dataSet: chartData.dataSets, style: chartData.dataSets.style, minValue: minValue, range: range, colours: colours, startPoint: startPoint, endPoint: endPoint, isFilled: true) + LineChartColoursSubView(chartData: chartData, + dataSet: chartData.dataSets, + minValue: minValue, + range: range, + colours: colours, + startPoint: startPoint, + endPoint: endPoint, + isFilled: true) } else if chartData.dataSets.style.colourType == .gradientStops, let stops = chartData.dataSets.style.stops, @@ -48,7 +66,14 @@ public struct FilledLineChart: View where ChartData: LineChartData { { let stops = GradientStop.convertToGradientStopsArray(stops: stops) - LineChartStopsSubView(chartData: chartData, dataSet: chartData.dataSets, style: chartData.dataSets.style, minValue: minValue, range: range, stops: stops, startPoint: startPoint, endPoint: endPoint, isFilled: true) + 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/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index 4f957df8..61394f6c 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -18,10 +18,15 @@ public struct LineChart: View where ChartData: LineChartData { public init(chartData: ChartData) { self.chartData = chartData - self.minValue = DataFunctions.minValue(dataPoints: chartData.dataSets.dataPoints) - self.range = DataFunctions.range(dataPoints: chartData.dataSets.dataPoints) - chartData.setupLegends() + switch chartData.chartStyle.baseline { + case .minimumValue: + self.minValue = chartData.getMinValue() + self.range = chartData.getRange() + case .zero: + self.minValue = 0 + self.range = chartData.getMaxValue() + } } public var body: some View { @@ -32,45 +37,42 @@ public struct LineChart: View where ChartData: LineChartData { let colour = chartData.dataSets.style.colour { LineChartColourSubView(chartData: chartData, - dataSet: chartData.dataSets, - style: chartData.dataSets.style, - minValue: minValue, - range: range, - colour: colour, - isFilled: false) + dataSet : chartData.dataSets, + minValue : minValue, + range : range, + colour : colour, + isFilled : false) } else if chartData.dataSets.style.colourType == .gradientColour, let colours = chartData.dataSets.style.colours, let startPoint = chartData.dataSets.style.startPoint, let endPoint = chartData.dataSets.style.endPoint { - - LineChartColoursSubView(chartData: chartData, - dataSet: chartData.dataSets, - style: chartData.dataSets.style, - minValue: minValue, - range: range, - colours: colours, - startPoint: startPoint, - endPoint: endPoint, - isFilled: false) - + + LineChartColoursSubView(chartData : chartData, + dataSet : chartData.dataSets, + minValue : minValue, + range : range, + colours : colours, + startPoint : startPoint, + endPoint : endPoint, + isFilled : false) + } else if chartData.dataSets.style.colourType == .gradientStops, let stops = chartData.dataSets.style.stops, let startPoint = chartData.dataSets.style.startPoint, let endPoint = chartData.dataSets.style.endPoint { let stops = GradientStop.convertToGradientStopsArray(stops: stops) - - LineChartStopsSubView(chartData: chartData, - dataSet: chartData.dataSets, - style: chartData.dataSets.style, - minValue: minValue, - range: range, - stops: stops, + + LineChartStopsSubView(chartData : chartData, + dataSet : chartData.dataSets, + minValue : minValue, + range : range, + stops : stops, startPoint: startPoint, - endPoint: endPoint, - isFilled: false) + endPoint : endPoint, + isFilled : false) } // } else { CustomNoDataView(chartData: chartData) } diff --git a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift index fe659609..7cd9dfa3 100644 --- a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift @@ -18,16 +18,21 @@ public struct MultiLineChart: View where ChartData: MultiLineChartDat public init(chartData: ChartData) { self.chartData = chartData - self.minValue = DataFunctions.multiDataSetMinValue(from: chartData.dataSets) - self.range = DataFunctions.multiDataSetRange(from: chartData.dataSets) - - chartData.setupLegends() + + switch chartData.chartStyle.baseline { + case .minimumValue: + self.minValue = chartData.getMinValue() + self.range = chartData.getRange() + case .zero: + self.minValue = 0 + self.range = chartData.getMaxValue() + } } public var body: some View { ZStack { - ForEach(chartData.dataSets.dataSets, id: \.self) { dataSet in + ForEach(chartData.dataSets.dataSets, id: \.id) { dataSet in // if chartData.isGreaterThanTwo { @@ -35,7 +40,12 @@ public struct MultiLineChart: View where ChartData: MultiLineChartDat let colour = dataSet.style.colour { - LineChartColourSubView(chartData: chartData, dataSet: dataSet, style: dataSet.style, minValue: minValue, range: range, colour: colour, isFilled: false) + LineChartColourSubView(chartData: chartData, + dataSet: dataSet, + minValue: minValue, + range: range, + colour: colour, + isFilled: false) } else if dataSet.style.colourType == .gradientColour, let colours = dataSet.style.colours, @@ -43,7 +53,14 @@ public struct MultiLineChart: View where ChartData: MultiLineChartDat let endPoint = dataSet.style.endPoint { - LineChartColoursSubView(chartData: chartData, dataSet: dataSet, style: dataSet.style, minValue: minValue, range: range, colours: colours, startPoint: startPoint, endPoint: endPoint, isFilled: false) + LineChartColoursSubView(chartData: chartData, + dataSet: dataSet, + minValue: minValue, + range: range, + colours: colours, + startPoint: startPoint, + endPoint: endPoint, + isFilled: false) } else if dataSet.style.colourType == .gradientStops, let stops = dataSet.style.stops, @@ -52,7 +69,14 @@ public struct MultiLineChart: View where ChartData: MultiLineChartDat { let stops = GradientStop.convertToGradientStopsArray(stops: stops) - LineChartStopsSubView(chartData: chartData, dataSet: dataSet, style: dataSet.style, minValue: minValue, range: range, stops: stops, startPoint: startPoint, endPoint: endPoint, isFilled: false) + LineChartStopsSubView(chartData: chartData, + dataSet: dataSet, + minValue: minValue, + range: range, + stops: stops, + startPoint: startPoint, + endPoint: endPoint, + isFilled: false) } } diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift index a585f078..96ad055f 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift @@ -7,11 +7,10 @@ import SwiftUI -struct LineChartColourSubView: View where CD: LineAndBarChartData { +internal struct LineChartColourSubView: View where CD: LineAndBarChartData { let chartData : CD let dataSet : LineDataSet - let style : LineStyle let minValue : Double let range : Double let colour : Color @@ -20,15 +19,34 @@ struct LineChartColourSubView: View where CD: LineAndBarChartData { @State var startAnimation : Bool = false - var body: some View { + internal init(chartData : CD, + dataSet : LineDataSet, + 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 + } + + internal var body: some View { - LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: isFilled, minValue: minValue, range: range) + LineShape(dataPoints: dataSet.dataPoints, + lineType : dataSet.style.lineType, + isFilled : isFilled, + minValue : minValue, + range : range) .ifElse(isFilled, if: { $0.scale(y: startAnimation ? 1 : 0, anchor: .bottom) .fill(colour) }, else: { $0.trim(to: startAnimation ? 1 : 0) - .stroke(colour, style: Stroke.strokeToStrokeStyle(stroke: style.strokeStyle)) + .stroke(colour, style: Stroke.strokeToStrokeStyle(stroke: dataSet.style.strokeStyle)) }) .background(Color(.gray).opacity(0.01)) @@ -43,11 +61,11 @@ struct LineChartColourSubView: View where CD: LineAndBarChartData { } } -struct LineChartColoursSubView: View where CD: LineAndBarChartData { +internal struct LineChartColoursSubView: View where CD: LineAndBarChartData { let chartData : CD let dataSet : LineDataSet - let style : LineStyle + let minValue : Double let range : Double let colours : [Color] @@ -58,10 +76,13 @@ struct LineChartColoursSubView: View where CD: LineAndBarChartData { @State var startAnimation : Bool = false - var body: some View { + internal var body: some View { - LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: isFilled, minValue: minValue, range: range) - + LineShape(dataPoints: dataSet.dataPoints, + lineType: dataSet.style.lineType, + isFilled: isFilled, + minValue: minValue, + range: range) .ifElse(isFilled, if: { $0 .scale(y: startAnimation ? 1 : 0, anchor: .bottom) @@ -74,7 +95,7 @@ struct LineChartColoursSubView: View where CD: LineAndBarChartData { .stroke(LinearGradient(gradient: Gradient(colors: colours), startPoint: startPoint, endPoint: endPoint), - style: Stroke.strokeToStrokeStyle(stroke: style.strokeStyle)) + style: Stroke.strokeToStrokeStyle(stroke: dataSet.style.strokeStyle)) }) @@ -90,11 +111,11 @@ struct LineChartColoursSubView: View where CD: LineAndBarChartData { } } -struct LineChartStopsSubView: View where CD: LineAndBarChartData { +internal struct LineChartStopsSubView: View where CD: LineAndBarChartData { let chartData : CD let dataSet : LineDataSet - let style : LineStyle + let minValue : Double let range : Double let stops : [Gradient.Stop] @@ -105,9 +126,13 @@ struct LineChartStopsSubView: View where CD: LineAndBarChartData { @State var startAnimation : Bool = false - var body: some View { + internal var body: some View { - LineShape(dataSet: dataSet, lineType: style.lineType, isFilled: isFilled, minValue: minValue, range: range) + LineShape(dataPoints: dataSet.dataPoints, + lineType: dataSet.style.lineType, + isFilled: isFilled, + minValue: minValue, + range: range) .ifElse(isFilled, if: { $0 @@ -121,7 +146,7 @@ struct LineChartStopsSubView: View where CD: LineAndBarChartData { .stroke(LinearGradient(gradient: Gradient(stops: stops), startPoint: startPoint, endPoint: endPoint), - style: Stroke.strokeToStrokeStyle(stroke: style.strokeStyle)) + style: Stroke.strokeToStrokeStyle(stroke: dataSet.style.strokeStyle)) }) .background(Color(.gray).opacity(0.01)) diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift index f56c63e6..6d041354 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift @@ -43,14 +43,10 @@ public class PieChartData: PieChartDataProtocol { 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 - - print(startAngle, amount) - startAngle += amount } } @@ -89,15 +85,11 @@ public class PieChartData: PieChartDataProtocol { func degree(from touchLocation: CGPoint, in rect: CGRect) -> CGFloat { - // http://www.cplusplus.com/reference/cmath/atan2/ // https://stackoverflow.com/a/25398191 - 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) { @@ -105,100 +97,8 @@ public class PieChartData: PieChartDataProtocol { } else { return -90 - degrees } - - - /* - // Where 0 is north - let degrees = atan2(-x, -y) * CGFloat(180 / Double.pi) - if (degrees > 0) { - return 360 - degrees - } else { - return 0 - degrees - } - - Where 0 is East - var degrees = atan2(y, x) * CGFloat(180 / Double.pi) - if (degrees < 0) { - degrees = 360 + degrees - } - return degrees - */ } public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint } - -public struct PieChartDataPoint: ChartDataPoint { - - public var id : UUID = UUID() - public var value : Double - public var xAxisLabel : String? - public var pointDescription : String? - public var date : Date? - - public var colour : Color - - var startAngle : Double = 0 - var amount : Double = 0 - - public init(value : Double, - xAxisLabel : String? = nil, - pointDescription: String? = nil, - date : Date? = nil, - colour : Color = Color.red - ) { - self.value = value - self.xAxisLabel = xAxisLabel - self.pointDescription = pointDescription - self.date = date - self.colour = colour - } -} - -public struct PieDataSet: SingleDataSet { - - public var id : UUID = UUID() - public var dataPoints : [PieChartDataPoint] - public var legendTitle : String - public var pointStyle : PointStyle - public var style : PieStyle - - public init(dataPoints : [PieChartDataPoint], - legendTitle : String, - pointStyle : PointStyle, - style : PieStyle - ) { - self.dataPoints = dataPoints - self.legendTitle = legendTitle - self.pointStyle = pointStyle - self.style = style - } - - public typealias Styling = PieStyle - public typealias DataPoint = PieChartDataPoint -} - -public struct PieStyle: 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? - - public init(colour : Color? = nil, - colours : [Color]? = nil, - stops : [GradientStop]? = nil, - startPoint : UnitPoint? = nil, - endPoint : UnitPoint? = nil - ) { - self.colourType = .colour - self.colour = colour - self.colours = colours - self.stops = stops - self.startPoint = startPoint - self.endPoint = endPoint - } -} diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartDataPoint.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartDataPoint.swift new file mode 100644 index 00000000..a3066551 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartDataPoint.swift @@ -0,0 +1,35 @@ +// +// PieChartDataPoint.swift +// +// +// Created by Will Dale on 01/02/2021. +// + +import SwiftUI + +public struct PieChartDataPoint: CTPieDataPoint { + + public var id : UUID = UUID() + public var value : Double + public var xAxisLabel : String? + public var pointDescription : String? + public var date : Date? + + public var colour : Color + + var startAngle : Double = 0 + var amount : Double = 0 + + public init(value : Double, + xAxisLabel : String? = nil, + pointDescription: String? = nil, + date : Date? = nil, + colour : Color = Color.red + ) { + self.value = value + self.xAxisLabel = xAxisLabel + self.pointDescription = pointDescription + self.date = date + self.colour = colour + } +} diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartStyle.swift index 755a1675..41d5c77c 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartStyle.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartStyle.swift @@ -12,21 +12,6 @@ public struct PieChartStyle: CTPieChartStyle { /// 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 @@ -34,31 +19,11 @@ public struct PieChartStyle: CTPieChartStyle { /// 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/PieChart/Models/PieDataSet.swift b/Sources/SwiftUICharts/PieChart/Models/PieDataSet.swift new file mode 100644 index 00000000..d271f7f6 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Models/PieDataSet.swift @@ -0,0 +1,31 @@ +// +// PieDataSet.swift +// +// +// Created by Will Dale on 01/02/2021. +// + +import SwiftUI + +public struct PieDataSet: SingleDataSet { + + public var id : UUID = UUID() + public var dataPoints : [PieChartDataPoint] + public var legendTitle : String + public var pointStyle : PointStyle + public var style : PieSegmentStyle + + public init(dataPoints : [PieChartDataPoint], + legendTitle : String, + pointStyle : PointStyle, + style : PieSegmentStyle + ) { + self.dataPoints = dataPoints + self.legendTitle = legendTitle + self.pointStyle = pointStyle + self.style = style + } + + public typealias Styling = PieSegmentStyle + public typealias DataPoint = PieChartDataPoint +} diff --git a/Sources/SwiftUICharts/PieChart/Models/PieSegmentStyle.swift b/Sources/SwiftUICharts/PieChart/Models/PieSegmentStyle.swift new file mode 100644 index 00000000..6d0e01ba --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Models/PieSegmentStyle.swift @@ -0,0 +1,32 @@ +// +// PieSegmentStyle.swift +// +// +// Created by Will Dale on 01/02/2021. +// + +import SwiftUI + +public struct PieSegmentStyle: 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? + + public init(colour : Color? = nil, + colours : [Color]? = nil, + stops : [GradientStop]? = nil, + startPoint : UnitPoint? = nil, + endPoint : UnitPoint? = nil + ) { + self.colourType = .colour + self.colour = colour + self.colours = colours + self.stops = stops + self.startPoint = startPoint + self.endPoint = endPoint + } +} diff --git a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift new file mode 100644 index 00000000..6876ddbc --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift @@ -0,0 +1,60 @@ +// +// DoughnutChart.swift +// +// +// Created by Will Dale on 01/02/2021. +// + +import SwiftUI + +public struct DoughnutChart: View where ChartData: PieChartData { + + @ObservedObject var chartData: ChartData + + private let strokeWidth : Double + + @State var startAnimation : Bool = false + + public init(chartData: ChartData, + strokeWidth: Double + ) { + self.chartData = chartData + + self.strokeWidth = strokeWidth + } + + var mask: some View { + Circle() + .strokeBorder(Color(.white), lineWidth: CGFloat(strokeWidth)) + } + + public var body: some View { + GeometryReader { geo in + + 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) + } + } + } + } + .mask(mask) + .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 index 9595dbcc..1d2efa31 100644 --- a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift @@ -15,27 +15,12 @@ public struct PieChart: View where ChartData: PieChartData { @State var startAnimation : Bool = false - public init(chartData: ChartData, - strokeWidth: Double? = nil - ) { + public init(chartData: ChartData) { self.chartData = chartData - - self.strokeWidth = strokeWidth } -// -// @ViewBuilder -// var mask: some View { -// if let strokeWidth = strokeWidth { -// Circle() -// .strokeBorder(Color(.white), lineWidth: CGFloat(strokeWidth)) -// } else { -// Circle() -// } -// } public var body: some View { GeometryReader { geo in - ZStack { ForEach(chartData.dataSets.dataPoints.indices, id: \.self) { data in PieSegmentShape(id: chartData.dataSets.dataPoints[data].id, @@ -54,12 +39,11 @@ public struct PieChart: View where ChartData: PieChartData { } } } -// .mask(mask) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = false - } + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = false + } } } diff --git a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift index 3d87f150..a065ebb3 100644 --- a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift +++ b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift @@ -9,44 +9,13 @@ import Foundation struct DataFunctions { - // MARK: - Just DataPoints - /// Get the highest value from dataPoints array. - /// - Returns: Highest value. - static func maxValue(dataPoints: [D]) -> Double { - return dataPoints.max { $0.value < $1.value }?.value ?? 0 - } - /// Get the Lowest value from dataPoints array. - /// - Returns: Lowest value. - static func minValue(dataPoints: [D]) -> Double { - return dataPoints.min { $0.value < $1.value }?.value ?? 0 - } - /// Get the average of all the dataPoints. - /// - Returns: Average. - static func average(dataPoints: [D]) -> 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. - static func range(dataPoints: [D]) -> 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 - } - // MARK: - Single Data Set static func dataSetMaxValue(from dataSets: T) -> Double { - return dataSets.dataPoints.max { $0.value < $1.value }?.value ?? 0 - + return dataSets.dataPoints.max { $0.value < $1.value }?.value ?? 0 } static func dataSetMinValue(from dataSets: T) -> Double { - return dataSets.dataPoints.min { $0.value < $1.value }?.value ?? 0 + return dataSets.dataPoints.min { $0.value < $1.value }?.value ?? 0 } static func dataSetAverage(from dataSets: T) -> Double { @@ -55,7 +24,6 @@ struct DataFunctions { } static func dataSetRange(from dataSets: T) -> Double { - let maxValue = dataSets.dataPoints.max { $0.value < $1.value }?.value ?? 0 let minValue = dataSets.dataPoints.min { $0.value < $1.value }?.value ?? 0 @@ -66,6 +34,7 @@ struct DataFunctions { return (maxValue - minValue) + 0.001 } + // MARK: - Multi Data Sets static func multiDataSetMaxValue(from dataSets: T) -> Double { var setHolder : [Double] = [] @@ -82,7 +51,7 @@ struct DataFunctions { } return setHolder.min { $0 < $1 } ?? 0 } - + static func multiDataSetAverage(from dataSets: T) -> Double { var setHolder : [Double] = [] for set in dataSets.dataSets { @@ -92,20 +61,20 @@ struct DataFunctions { let sum = setHolder.reduce(0) { $0 + $1 } return sum / Double(setHolder.count) } - + static func multiDataSetRange(from dataSets: T) -> Double { var setMaxHolder : [Double] = [] for set in dataSets.dataSets { setMaxHolder.append(set.dataPoints.max { $0.value < $1.value }?.value ?? 0) } let maxValue = setMaxHolder.max { $0 < $1 } ?? 0 - + var setMinHolder : [Double] = [] for set in dataSets.dataSets { setMinHolder.append(set.dataPoints.min { $0.value < $1.value }?.value ?? 0) } let minValue = setMinHolder.min { $0 < $1 } ?? 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. diff --git a/Sources/SwiftUICharts/Shared/Extras/Enums.swift b/Sources/SwiftUICharts/Shared/Extras/Enums.swift index 53cd5d45..c609c2c8 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Enums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Enums.swift @@ -30,6 +30,10 @@ public enum CalculationType { } // MARK: - ChartViewData +public enum DataSetType { + case single + case multi +} /** Pass the type of chart being used to view modifiers. ``` diff --git a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift index 8bc4956a..a1eb3fae 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift @@ -19,7 +19,7 @@ public struct ChartViewData { } /// Data model to pass view information internally so the layout can configure its self. -public struct InfoViewData { +public struct InfoViewData { /** Is there currently input (touch or click) on the chart diff --git a/Sources/SwiftUICharts/Shared/Models/LegendData.swift b/Sources/SwiftUICharts/Shared/Models/LegendData.swift index 809f3ed7..8e9393ad 100644 --- a/Sources/SwiftUICharts/Shared/Models/LegendData.swift +++ b/Sources/SwiftUICharts/Shared/Models/LegendData.swift @@ -12,28 +12,29 @@ public struct LegendData: CTColourStyle, Hashable, Identifiable { public var id : UUID - var chartType : ChartType - public var colourType : ColourType + public var chartType : ChartType + /// Text to be displayed - var legend : String + public var legend : String /// Style of the stroke - var strokeStyle : Stroke? + public var strokeStyle : Stroke? + public var colourType : ColourType /// Single Colour - public var colour : Color? + public var colour : Color? /// Colours for Gradient - public var colours : [Color]? + public var colours : [Color]? /// Colours and Stops for Gradient with stop control - public var stops : [GradientStop]? + public var stops : [GradientStop]? /// Start point for Gradient - public var startPoint : UnitPoint? + public var startPoint : UnitPoint? /// End point for Gradient - public var endPoint : UnitPoint? + public var endPoint : UnitPoint? /// Used to make sure the charts data legend is first - let prioity : Int + public let prioity : Int /// Legend with single colour /// - Parameters: diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols.swift index 07457aef..c61e72f8 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols.swift @@ -9,7 +9,7 @@ import SwiftUI public protocol ChartData: ObservableObject, Identifiable { associatedtype Set : DataSet - associatedtype DataPoint: ChartDataPoint + associatedtype DataPoint: CTChartDataPoint var id : UUID { get } var dataSets : Set { get set } @@ -46,35 +46,108 @@ extension ChartData { return legends.sorted { $0.prioity < $1.prioity} } } -public protocol LineChartProtocol {} + public protocol LineAndBarChartData : ChartData { - associatedtype Style : CTLineAndBarChartStyle - associatedtype Body : View - var chartStyle : Style { get set } + associatedtype Body : View + associatedtype CTStyle : CTLineAndBarChartStyle var viewData : ChartViewData { get set } + var chartStyle : CTStyle { get set } func getXAxidLabels() -> Body func getYLabels() -> [Double] + func getRange() -> Double func getMinValue() -> Double func getMaxValue() -> Double func getAverage() -> Double } + +extension LineAndBarChartData where Self: LineChartDataProtocol { + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let dataRange : Double + let minValue : Double + let range : Double + + switch self.chartStyle.baseline { + case .minimumValue: + minValue = self.getMinValue() + dataRange = self.getRange() + range = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) + case .zero: + minValue = 0 + dataRange = self.getMaxValue() + range = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) + } + labels.append(minValue) + for index in 1...self.chartStyle.yAxisNumberOfLabels { + labels.append(minValue + range * Double(index)) + } + return labels + } +} +extension LineAndBarChartData where Self: BarChartDataProtocol { + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let maxValue: Double = self.getMaxValue() + for index in 0...self.chartStyle.yAxisNumberOfLabels { + labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) + } + return labels + } +} + +extension LineAndBarChartData where Set: SingleDataSet { + public func getRange() -> Double { + DataFunctions.dataSetRange(from: dataSets) + } + public func getMinValue() -> Double { + DataFunctions.dataSetMinValue(from: dataSets) + } + public func getMaxValue() -> Double { + DataFunctions.dataSetMaxValue(from: dataSets) + } + public func getAverage() -> Double { + DataFunctions.dataSetAverage(from: dataSets) + } +} +extension LineAndBarChartData where Set: MultiDataSet { + public func getRange() -> Double { + DataFunctions.multiDataSetRange(from: dataSets) + } + public func getMinValue() -> Double { + DataFunctions.multiDataSetMinValue(from: dataSets) + } + public func getMaxValue() -> Double { + DataFunctions.multiDataSetMaxValue(from: dataSets) + } + public func getAverage() -> Double { + DataFunctions.multiDataSetAverage(from: dataSets) + } +} + +public protocol LineChartDataProtocol: LineAndBarChartData { + associatedtype Style : CTLineChartStyle + var chartStyle : Style { get set } +} +public protocol BarChartDataProtocol: LineAndBarChartData { + associatedtype Style : CTBarChartStyle + var chartStyle : Style { get set } +} public protocol PieChartDataProtocol : ChartData { associatedtype Style : CTPieChartStyle - var chartStyle : Style { get set } } - +// MARK: - Data Sets public protocol DataSet: Hashable, Identifiable { var id : ID { get } } public protocol SingleDataSet: DataSet { associatedtype Styling : CTColourStyle - associatedtype DataPoint : ChartDataPoint + associatedtype DataPoint : CTChartDataPoint var dataPoints : [DataPoint] { get set } var legendTitle : String { get set } @@ -83,12 +156,11 @@ public protocol SingleDataSet: DataSet { } public protocol MultiDataSet: DataSet { associatedtype DataSet : SingleDataSet - var dataSets : [DataSet] { get set } } - +// MARK: - Styles public protocol CTChartStyle { var infoBoxPlacement : InfoBoxPlacement { get set } var globalAnimation : Animation { get set } @@ -101,7 +173,13 @@ public protocol CTLineAndBarChartStyle: CTChartStyle { var yAxisLabelPosition : YAxisLabelPosistion { get set } var yAxisNumberOfLabels : Int { get set } } -public protocol CTPieChartStyle: CTChartStyle {} +public protocol CTLineChartStyle : CTLineAndBarChartStyle { + var baseline : Baseline { get set } +} +public protocol CTBarChartStyle : CTLineAndBarChartStyle { +} +public protocol CTPieChartStyle: CTChartStyle { +} public protocol CTColourStyle { var colourType : ColourType { get set } @@ -112,18 +190,18 @@ public protocol CTColourStyle { var endPoint : UnitPoint? { get set } } - -public protocol ChartDataPoint: Hashable, Identifiable { +// MARK: - Data Points +public protocol CTChartDataPoint: Hashable, Identifiable { var id : ID { get } var value : Double { get set } - var xAxisLabel : String? { get set } // Not Pie var pointDescription : String? { get set } var date : Date? { get set } } - -public enum DataSetType { - case single - case multi +public protocol CTLineAndBarDataPoint: CTChartDataPoint { + var xAxisLabel : String? { get set } } +public protocol CTPieDataPoint: CTChartDataPoint { + +} diff --git a/Sources/SwiftUICharts/Shared/Models/Stroke.swift b/Sources/SwiftUICharts/Shared/Models/Stroke.swift index d864e20e..2b0a1b25 100644 --- a/Sources/SwiftUICharts/Shared/Models/Stroke.swift +++ b/Sources/SwiftUICharts/Shared/Models/Stroke.swift @@ -8,8 +8,9 @@ import SwiftUI /// Replica of Apple's `StrokeStyle` that conforms to `Hashable` -public struct Stroke: Hashable { +public struct Stroke: Hashable, Identifiable { + public let id : UUID = UUID() var lineWidth : CGFloat var lineCap : CGLineCap var lineJoin : CGLineJoin diff --git a/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift b/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift index 82d9bdda..b9b8771a 100644 --- a/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift +++ b/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift @@ -11,17 +11,14 @@ internal struct Point: Shape where T: SingleDataSet { private let dataSet : T - private let maxValue : Double private let minValue : Double private let range : Double internal init(dataSet : T, - maxValue : Double, minValue : Double, range : Double ) { self.dataSet = dataSet - self.maxValue = maxValue self.minValue = minValue self.range = range } @@ -32,7 +29,7 @@ internal struct Point: Shape where T: SingleDataSet { return path } - internal func lineChartDrawPoints(_ path: inout Path, _ rect: CGRect, _ dataPoints: [DP], _ minValue: Double, _ range: Double) { + internal func lineChartDrawPoints(_ path: inout Path, _ rect: CGRect, _ dataPoints: [DP], _ minValue: Double, _ range: Double) { let x = rect.width / CGFloat(dataPoints.count-1) let y = rect.height / CGFloat(range) diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift index 5cad9e9f..80f00360 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift @@ -15,12 +15,9 @@ internal struct Legends: ViewModifier where T: ChartData { VStack { content LegendView(chartData: chartData) - } } } - - extension View { /// Displays legends under the chart. diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift index 508f3445..fae210c0 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift @@ -7,37 +7,40 @@ import SwiftUI -internal struct PointMarkers: ViewModifier where T: LineAndBarChartData & LineChartProtocol { +internal struct PointMarkers: ViewModifier where T: LineChartDataProtocol { @ObservedObject var chartData: T - private let maxValue : Double private let minValue : Double private let range : Double internal init(chartData : T) { self.chartData = chartData - self.maxValue = chartData.getMaxValue() - self.minValue = chartData.getMinValue() - self.range = chartData.getRange() + switch chartData.chartStyle.baseline { + case .minimumValue: + self.minValue = chartData.getMinValue() + self.range = chartData.getRange() + case .zero: + self.minValue = 0 + self.range = chartData.getMaxValue() + } + + } internal func body(content: Content) -> some View { - ZStack { content if chartData.chartType.dataSetType == .single { let data = chartData as! LineChartData - PointsSubView(dataSets: data.dataSets, maxValue: maxValue, minValue: minValue, range: range) + PointsSubView(dataSets: data.dataSets, minValue: minValue, range: range) } else if chartData.chartType.dataSetType == .multi { let data = chartData as! MultiLineChartData ForEach(data.dataSets.dataSets, id: \.self) { dataSet in -// if chartData.isGreaterThanTwo { - PointsSubView(dataSets: dataSet, maxValue: maxValue, minValue: minValue, range: range) -// } + PointsSubView(dataSets: dataSet, minValue: minValue, range: range) } } } @@ -48,7 +51,7 @@ 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(chartData: T) -> some View { + public func pointMarkers(chartData: T) -> some View { self.modifier(PointMarkers(chartData: chartData)) } } @@ -56,7 +59,6 @@ extension View { struct PointsSubView: View { let dataSets: LineDataSet - let maxValue : Double let minValue : Double let range : Double @@ -64,24 +66,20 @@ struct PointsSubView: View { switch dataSets.pointStyle.pointType { case .filled: Point(dataSet : dataSets, - maxValue : maxValue, minValue : minValue, range : range) .fill(dataSets.pointStyle.fillColour) case .outline: Point(dataSet : dataSets, - maxValue : maxValue, minValue : minValue, range : range) .stroke(dataSets.pointStyle.borderColour, lineWidth: dataSets.pointStyle.lineWidth) case .filledOutLine: Point(dataSet : dataSets, - maxValue : maxValue, minValue : minValue, range : range) .stroke(dataSets.pointStyle.borderColour, lineWidth: dataSets.pointStyle.lineWidth) .background(Point(dataSet : dataSets, - maxValue : maxValue, minValue : minValue, range : range) .foregroundColor(dataSets.pointStyle.fillColour) diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 475704b3..9b87fce7 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -51,12 +51,12 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { .onChanged { (value) in touchLocation = value.location + chartData.infoView.isTouchCurrent = true + self.selectedPoints = chartData.getDataPoint(touchLocation: touchLocation, chartSize: geo) - self.pointLocations = chartData.getPointLocation(touchLocation: touchLocation, chartSize: geo) - if chartData.getHeaderLocation() == .floating { setBoxLocationation(boxFrame: boxFrame, chartSize: geo) @@ -65,7 +65,6 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { } else if chartData.getHeaderLocation() == .header { - chartData.infoView.isTouchCurrent = true chartData.infoView.touchOverlayInfo = selectedPoints } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift index b6a5b2c6..9ffae1b5 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift @@ -19,7 +19,6 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { ) { self.chartData = chartData self.specifier = specifier - chartData.viewData.hasYAxisLabels = true } diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index 2f7e051d..4bcfb520 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -25,12 +25,12 @@ internal struct LegendView: View where T: ChartData { internal var body: some View { LazyVGrid(columns: columns, alignment: .leading) { - ForEach(chartData.legendOrder(), id: \.self) { legend in + ForEach(chartData.legendOrder(), id: \.id) { legend in switch legend.chartType { - + case .line: - + line(legend) case .bar: @@ -38,7 +38,7 @@ internal struct LegendView: View where T: ChartData { bar(legend) case .pie: - + pie(legend) .if(chartData.infoView.isTouchCurrent && legend.id == chartData.infoView.touchOverlayInfo[0].id as! UUID) { $0.scaleEffect(1.2, anchor: .leading) } } diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index 0f158180..29cc4b8e 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -7,7 +7,7 @@ import SwiftUI -internal struct TouchOverlayBox: View { +internal struct TouchOverlayBox: View { private var selectedPoints : [D] private var specifier : String @@ -36,8 +36,6 @@ internal struct TouchOverlayBox: View { } if let label = point.pointDescription { Text(label) - } else if let label = point.xAxisLabel { - Text(label) } } } From 4e2f7a0299def8db9cc808f8730aab74724a79b3 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Tue, 2 Feb 2021 11:32:26 +0000 Subject: [PATCH 017/152] Tidy up and refactor protocols. --- .../BarChart/Models/BarChartData.swift | 8 +- .../BarChart/Models/MultiBarChartData.swift | 4 - .../LineChart/Models/LineChartData.swift | 49 +-- .../LineChart/Models/MultiLineChartData.swift | 7 +- .../LineChart/Views/FilledLineChart.swift | 1 + .../LineChart/Views/LineChartView.swift | 4 +- .../Views/SubViews/LineChartSubViews.swift | 1 - .../PieChart/Models/PieChartData.swift | 89 ++--- .../PieChart/Models/PieChartStyle.swift | 25 ++ .../PieChart/Shapes/PieSegmentShape.swift | 30 ++ .../PieChart/Views/DoughnutChart.swift | 39 +- .../PieChart/Views/PieChart.swift | 4 +- .../SwiftUICharts/Shared/Extras/Enums.swift | 71 +++- .../Shared/Models/LegendData.swift | 1 + .../Shared/Models/Protocols.swift | 338 +++++++++++++----- .../Shared/ViewModifiers/PointMarkers.swift | 61 +++- .../Shared/ViewModifiers/YAxisPOI.swift | 32 +- 17 files changed, 512 insertions(+), 252 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index 8241726a..c261a980 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -54,10 +54,6 @@ public class BarChartData: BarChartDataProtocol { self.setupLegends() } - public func getHeaderLocation() -> InfoBoxPlacement { - return self.chartStyle.infoBoxPlacement - } - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { var points : [BarChartDataPoint] = [] let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count) @@ -91,8 +87,8 @@ public class BarChartData: BarChartDataProtocol { .font(.caption) .lineLimit(1) .minimumScaleFactor(0.5) - Spacer() - .frame(minWidth: 0, maxWidth: 500) + Spacer() + .frame(minWidth: 0, maxWidth: 500) } } } diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift index 28ae0e87..c1e4614d 100644 --- a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift @@ -54,10 +54,6 @@ public class MultiBarChartData: BarChartDataProtocol { self.setupLegends() } - public func getHeaderLocation() -> InfoBoxPlacement { - return self.chartStyle.infoBoxPlacement - } - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { var points : [BarChartDataPoint] = [] for dataSet in dataSets.dataSets { diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index d3a701f5..eaf2c072 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -12,18 +12,14 @@ public class LineChartData: LineChartDataProtocol { public let id : UUID = UUID() - /// Data model containing the datapoints: Value, Label, Description and Date. Individual colouring for bar chart. @Published public var dataSets : LineDataSet - /// 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 : LineChartStyle - /// Array of data to populate the chart legend. @Published public var legends : [LegendData] - /// Data model to hold data about the Views layout. @Published public var viewData : ChartViewData + @Published public var isFilled : Bool = false @Published public var infoView : InfoViewData = InfoViewData() @@ -65,8 +61,23 @@ public class LineChartData: LineChartDataProtocol { self.setupLegends() } - public func getHeaderLocation() -> InfoBoxPlacement { - return self.chartStyle.infoBoxPlacement + // MARK: Labels + public func getXAxidLabels() -> some View { + HStack(spacing: 0) { + ForEach(dataSets.dataPoints) { data in + if let label = data.xAxisLabel { + Text(label) + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + } + if data != self.dataSets.dataPoints[self.dataSets.dataPoints.count - 1] { + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } + .padding(.horizontal, -4) } // MARK: Touch @@ -147,27 +158,3 @@ public class LineChartData: LineChartDataProtocol { public typealias Set = LineDataSet public typealias DataPoint = LineChartDataPoint } - -//MARK: - LineAndBarChartData - -extension LineChartData { - - // MARK: Labels - public func getXAxidLabels() -> some View { - HStack(spacing: 0) { - ForEach(dataSets.dataPoints) { data in - if let label = data.xAxisLabel { - Text(label) - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) - } - if data != self.dataSets.dataPoints[self.dataSets.dataPoints.count - 1] { - Spacer() - .frame(minWidth: 0, maxWidth: 500) - } - } - } - .padding(.horizontal, -4) - } -} diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift index 17decd66..9f076ca8 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -29,7 +29,8 @@ public class MultiLineChartData: LineChartDataProtocol { /// Data model to hold data about the Views layout. @Published public var viewData : ChartViewData - + @Published public var isFilled : Bool = false + @Published public var infoView : InfoViewData = InfoViewData() public var noDataText : Text = Text("No Data") @@ -67,10 +68,6 @@ public class MultiLineChartData: LineChartDataProtocol { self.chartType = (chartType: .line, dataSetType: .multi) self.setupLegends() } - - public func getHeaderLocation() -> InfoBoxPlacement { - return self.chartStyle.infoBoxPlacement - } public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [LineChartDataPoint] { var points : [LineChartDataPoint] = [] diff --git a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift index 89a34b68..e5722ac0 100644 --- a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift @@ -27,6 +27,7 @@ public struct FilledLineChart: View where ChartData: LineChartData { self.minValue = 0 self.range = chartData.getMaxValue() } + self.chartData.isFilled = true } public var body: some View { diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index 61394f6c..d22e6661 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -13,9 +13,7 @@ public struct LineChart: View where ChartData: LineChartData { private let minValue : Double private let range : Double - - @State var startAnimation : Bool = false - + public init(chartData: ChartData) { self.chartData = chartData diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift index 96ad055f..821ee3f3 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift @@ -48,7 +48,6 @@ internal struct LineChartColourSubView: View where CD: LineAndBarChartData { $0.trim(to: startAnimation ? 1 : 0) .stroke(colour, style: Stroke.strokeToStrokeStyle(stroke: dataSet.style.strokeStyle)) }) - .background(Color(.gray).opacity(0.01)) .if(chartData.viewData.hasXAxisLabels) { $0.xAxisBorder(chartData: chartData) } .if(chartData.viewData.hasYAxisLabels) { $0.yAxisBorder(chartData: chartData) } diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift index 6d041354..d1ebfdfb 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift @@ -40,65 +40,46 @@ public class PieChartData: PieChartDataProtocol { self.makeDataPoints() } - 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 - } - } - public func getHeaderLocation() -> InfoBoxPlacement { - return self.chartStyle.infoBoxPlacement - } + public typealias Set = PieDataSet + public typealias DataPoint = PieChartDataPoint +} - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [PieChartDataPoint] { - var points : [PieChartDataPoint] = [] - let touchDegree = degree(from: touchLocation, in: chartSize.frame(in: .local)) - 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 { - points.append(data) - } - return points - } - - public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - - return [HashablePoint(x: touchLocation.x, y: touchLocation.y)] - } - - public func setupLegends() { - for data in dataSets.dataPoints { - if let legend = data.pointDescription { - self.legends.append(LegendData(id : data.id, - legend : legend, - colour : data.colour, - strokeStyle: nil, - prioity : 1, - chartType : .pie)) - } - } - } +public class DoughnutChartData: DoughnutChartDataProtocol { + + @Published public var id : UUID = UUID() + @Published public var dataSets : PieDataSet + @Published public var metadata : ChartMetadata? + @Published public var xAxisLabels : [String]? + @Published public var chartStyle : DoughnutChartStyle + @Published public var legends : [LegendData] + @Published public var infoView : InfoViewData + + let strokeWidth: CGFloat = 30 + public var noDataText: Text + public var chartType: (chartType: ChartType, dataSetType: DataSetType) - func degree(from touchLocation: CGPoint, in rect: CGRect) -> CGFloat { - // http://www.cplusplus.com/reference/cmath/atan2/ - // https://stackoverflow.com/a/25398191 - 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 - } + public init(dataSets : PieDataSet, + metadata : ChartMetadata? = nil, + xAxisLabels : [String]? = nil, + chartStyle : DoughnutChartStyle = DoughnutChartStyle(), + noDataText : Text + ) { + self.dataSets = dataSets + self.metadata = metadata + self.xAxisLabels = xAxisLabels + self.chartStyle = chartStyle + self.legends = [LegendData]() + self.infoView = InfoViewData() + self.noDataText = noDataText + self.chartType = (chartType: .pie, dataSetType: .single) + + self.setupLegends() + + self.makeDataPoints() } - + public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint } diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartStyle.swift index 41d5c77c..87a5075f 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartStyle.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartStyle.swift @@ -27,3 +27,28 @@ public struct PieChartStyle: CTPieChartStyle { self.globalAnimation = globalAnimation } } + +/// Model for controlling the overall aesthetic of the chart. +public struct DoughnutChartStyle: CTDoughnutChartStyle { + + /// Placement of the information box that appears on touch input. + public var infoBoxPlacement : InfoBoxPlacement + + /// Gobal control of animations. + 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. + /// - globalAnimation: Gobal control of animations. + public init(infoBoxPlacement : InfoBoxPlacement = .floating, + globalAnimation : Animation = Animation.linear(duration: 1), + strokeWidth : CGFloat = 30 + ) { + self.infoBoxPlacement = infoBoxPlacement + self.globalAnimation = globalAnimation + self.strokeWidth = strokeWidth + } +} diff --git a/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift b/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift index b7f02512..72c46f07 100644 --- a/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift +++ b/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift @@ -28,3 +28,33 @@ internal struct PieSegmentShape: Shape, Identifiable { return path } } + + +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/Views/DoughnutChart.swift b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift index 6876ddbc..679e9f44 100644 --- a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift @@ -7,36 +7,24 @@ import SwiftUI -public struct DoughnutChart: View where ChartData: PieChartData { +public struct DoughnutChart: View where ChartData: DoughnutChartData { @ObservedObject var chartData: ChartData - - private let strokeWidth : Double - + @State var startAnimation : Bool = false - public init(chartData: ChartData, - strokeWidth: Double - ) { + public init(chartData : ChartData) { self.chartData = chartData - - self.strokeWidth = strokeWidth - } - - var mask: some View { - Circle() - .strokeBorder(Color(.white), lineWidth: CGFloat(strokeWidth)) } public var body: some View { GeometryReader { geo in - 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) + DoughnutSegmentShape(id: chartData.dataSets.dataPoints[data].id, + startAngle: chartData.dataSets.dataPoints[data].startAngle, + amount: chartData.dataSets.dataPoints[data].amount) + .strokeBorder(chartData.dataSets.dataPoints[data].colour, lineWidth: chartData.strokeWidth) .scaleEffect(startAnimation ? 1 : 0) .opacity(startAnimation ? 1 : 0) .animation(Animation.spring().delay(Double(data) * 0.06)) @@ -49,12 +37,11 @@ public struct DoughnutChart: View where ChartData: PieChartData { } } } - .mask(mask) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = false - } + .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 index 1d2efa31..e49e5fd7 100644 --- a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift @@ -10,9 +10,7 @@ import SwiftUI public struct PieChart: View where ChartData: PieChartData { @ObservedObject var chartData: ChartData - - let strokeWidth : Double? - + @State var startAnimation : Bool = false public init(chartData: ChartData) { diff --git a/Sources/SwiftUICharts/Shared/Extras/Enums.swift b/Sources/SwiftUICharts/Shared/Extras/Enums.swift index c609c2c8..4b079ca8 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Enums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Enums.swift @@ -17,6 +17,8 @@ import Foundation case averageWeek // Weekly Average case averageDay // Daily Average ``` + + - Tag: CalculationType */ public enum CalculationType { /// No function @@ -30,23 +32,36 @@ public enum CalculationType { } // MARK: - ChartViewData +/** + The type of `DataSet` being used + ``` + case single // Single data set - i.e LineDataSet + case multi // Multi data set - i.e MultiLineDataSet + ``` + + - Tag: DataSetType + */ public enum DataSetType { case single case multi } + /** - Pass the type of chart being used to view modifiers. + The type of chart being used. ``` case line // Line Chart Type case bar // Bar Chart Type + case pie // Pie Chart Type ``` + + - Tag: ChartType */ public enum ChartType { /// Line Chart Type case line /// Bar Chart Type case bar - + /// Pie Chart Type case pie } @@ -58,6 +73,8 @@ public enum ChartType { case gradientColour // Colour Gradient case gradientStops // Colour Gradient with stop control ``` + + - Tag: ColourType */ public enum ColourType { /// Single Colour @@ -75,6 +92,8 @@ public enum ColourType { case line // Straight line from point to point case curvedLine // Dual control point curved line ``` + + - Tag: LineType */ public enum LineType { /// Straight line from point to point @@ -90,13 +109,31 @@ public enum LineType { case barStyle // From BarStyle data model case dataPoints // From each data point ``` + + - Tag: ColourFrom */ public enum ColourFrom { case barStyle case dataPoints } -// MARK: - TouchOverlayMarker +// 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() + ``` + + - Tag: InfoBoxPlacement + */ +public enum InfoBoxPlacement { + /// Follows input across the chart + case floating + /// Fix in the Header box. Must have .headerBox() + case header +} + /** Where the marker lines come from to meet at a specified point. ``` @@ -106,6 +143,8 @@ public enum ColourFrom { case topLeading // From top and leading edges meeting at touch location case topTrailing // From top and trailing edges meeting at touch location ``` + + - Tag: MarkerLineType */ public enum MarkerLineType { /// Full width and height of view intersecting at a specified point @@ -128,6 +167,8 @@ public enum MarkerLineType { case outline // Just stroke case filledOutLine // Both fill and stroke ``` + + - Tag: PointType */ public enum PointType { /// Just fill @@ -144,6 +185,8 @@ public enum PointType { case square case roundSquare ``` + + - Tag: PointShape */ public enum PointShape { /// Circle Shape @@ -154,22 +197,6 @@ public enum PointShape { 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 @@ -177,6 +204,8 @@ Location of the X axis labels case top case bottom ``` + + - Tag: XAxisLabelPosistion */ public enum XAxisLabelPosistion { case top @@ -192,6 +221,8 @@ public enum XAxisLabelPosistion { case dataPoint // ChartData --> DataPoint --> xAxisLabel case chartData // ChartData --> xAxisLabels ``` + + - Tag: LabelsFrom */ public enum LabelsFrom { /// ChartData --> DataPoint --> xAxisLabel @@ -207,6 +238,8 @@ Location of the Y axis labels case leading case trailing ``` + + - Tag: YAxisLabelPosistion */ public enum YAxisLabelPosistion { case leading diff --git a/Sources/SwiftUICharts/Shared/Models/LegendData.swift b/Sources/SwiftUICharts/Shared/Models/LegendData.swift index 8e9393ad..b59ea5aa 100644 --- a/Sources/SwiftUICharts/Shared/Models/LegendData.swift +++ b/Sources/SwiftUICharts/Shared/Models/LegendData.swift @@ -8,6 +8,7 @@ import SwiftUI /// Data model for Legends +/// - Tag: LegendData public struct LegendData: CTColourStyle, Hashable, Identifiable { public var id : UUID diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols.swift index c61e72f8..d87ae4a3 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols.swift @@ -7,50 +7,108 @@ import SwiftUI +/// The main public protocol ChartData: ObservableObject, Identifiable { associatedtype Set : DataSet associatedtype DataPoint: CTChartDataPoint + var id : UUID { get } + /// Data model containing the datapoints. + /// - Note: + /// `Set` is either `SingleData` or `MultiDataSet`. var dataSets : Set { get set } + + /// Data model containing: the charts Title, the charts Subtitle and the Line Legend. var metadata : ChartMetadata? { get set } - var xAxisLabels : [String]? { get set } // Not pie + + /// Array of strings for the labels on the X Axis instead of the labels in the data points. + /// - Note: + /// To control where the labels should come from; set `xAxisLabelsFrom` in `ChartStyle`. + var xAxisLabels : [String]? { get set } // Not Pie + + /// Array of `LegendData` to populate the chart legend. + /// - Note: + /// This is populated automatically from within each view. var legends : [LegendData] { get set } + + /// Data model to hold temporary data from `TouchOverlay` ViewModifier and pass the data points to display in the `HeaderView`. var infoView : InfoViewData { get set } + + /// Customisable `Text` to display when where is not enough data to draw the chart. var noDataText : Text { get set } + + /** + Holds metadata about the chart. + + Allows for internal logic based on the type of chart. + + This might get removed in favour of a more protocol based approach. + + # Reference + [ChartType](x-source-tag://ChartType) + + [DataSetType](x-source-tag://DataSetType) + */ var chartType : (chartType: ChartType, dataSetType: DataSetType) { get } - // Sets the order the Legends are layed out in. - /// - Returns: Ordered array of Legends. + /** + Sets the order the Legends are layed out in. + - Returns: Ordered array of Legends. + + # Reference + [LegendData](x-source-tag://LegendData) + - Tag: legendOrder + */ func legendOrder() -> [LegendData] + + /** + Gets the where to display the touch overlay information. + - Returns: Where to display the data points + + # Reference + [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) + + - Tag: getHeaderLocation + */ func getHeaderLocation() -> InfoBoxPlacement - /// Gets the nearest data point to the touch location based on the X axis. - /// - Parameters: - /// - touchLocation: Current location of the touch - /// - chartSize: The size of the chart view as the parent view. + /** + 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. + + - Tag: getDataPoint + */ func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [DataPoint] - /// Gets the location of the data point in the view. - /// - Parameters: - /// - touchLocation: Current location of the touch - /// - chartSize: The size of the chart view as the parent view. + /** + Gets the location of the data point in the view. + - Parameters: + - 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 + + - Tag: getDataPoint + */ func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] + /// Configures the legends based on the type of chart. func setupLegends() } -extension ChartData { - public func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } -} + public protocol LineAndBarChartData : ChartData { associatedtype Body : View associatedtype CTStyle : CTLineAndBarChartStyle + /// 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 } var chartStyle : CTStyle { get set } @@ -62,7 +120,124 @@ public protocol LineAndBarChartData : ChartData { func getMaxValue() -> Double func getAverage() -> Double } +public protocol LineChartDataProtocol: LineAndBarChartData where CTStyle: CTLineChartStyle { + var chartStyle : CTStyle { get set } + var isFilled : Bool { get set} +} +public protocol BarChartDataProtocol: LineAndBarChartData where CTStyle: CTBarChartStyle { + var chartStyle : CTStyle { get set } +} + + + + +public protocol PieAndDoughnutChartDataProtocol: ChartData { + associatedtype CTStyle : CTPieAndDoughnutChartStyle + var chartStyle : CTStyle { get set } +} + +public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol where CTStyle: CTPieChartStyle { + var chartStyle : CTStyle { get set } +} +public protocol DoughnutChartDataProtocol : PieAndDoughnutChartDataProtocol where CTStyle: CTDoughnutChartStyle { + var chartStyle : CTStyle { get set } +} + + + +// MARK: - Data Sets +public protocol DataSet: Hashable, Identifiable { + var id : ID { get } +} +public protocol SingleDataSet: DataSet { + associatedtype Styling : CTColourStyle + associatedtype DataPoint : CTChartDataPoint + + var dataPoints : [DataPoint] { get set } + var legendTitle : String { get set } + var pointStyle : PointStyle { get set } + var style : Styling { get set } +} +public protocol MultiDataSet: DataSet { + associatedtype DataSet : SingleDataSet + var dataSets : [DataSet] { get set } +} + + + + +// MARK: - Styles +public protocol CTChartStyle { + var infoBoxPlacement : InfoBoxPlacement { get set } + var globalAnimation : Animation { get set } +} + + +public protocol CTLineAndBarChartStyle: CTChartStyle { + var xAxisGridStyle : GridStyle { get set } + var yAxisGridStyle : GridStyle { get set } + var xAxisLabelPosition : XAxisLabelPosistion { get set } + var xAxisLabelsFrom : LabelsFrom { get set } + var yAxisLabelPosition : YAxisLabelPosistion { get set } + var yAxisNumberOfLabels : Int { get set } +} +public protocol CTLineChartStyle : CTLineAndBarChartStyle { + var baseline : Baseline { get set } +} +public protocol CTBarChartStyle : CTLineAndBarChartStyle {} + + +public protocol CTPieAndDoughnutChartStyle: CTChartStyle {} + +public protocol CTPieChartStyle: CTPieAndDoughnutChartStyle {} + +public protocol CTDoughnutChartStyle: CTPieAndDoughnutChartStyle { + var strokeWidth : CGFloat { get set } +} + +public protocol CTColourStyle { + var colourType : ColourType { get set } + var colour : Color? { get set } + var colours : [Color]? { get set } + var stops : [GradientStop]? { get set } + var startPoint : UnitPoint? { get set } + var endPoint : UnitPoint? { get set } +} + + + + +// MARK: - Data Points +public protocol CTChartDataPoint: Hashable, Identifiable { + var id : ID { get } + var value : Double { get set } + var pointDescription : String? { get set } + var date : Date? { get set } +} +public protocol CTLineAndBarDataPoint: CTChartDataPoint { + var xAxisLabel : String? { get set } +} +public protocol CTPieDataPoint: CTChartDataPoint { + +} + + + +// MARK: - Extensions + +extension ChartData { + public func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } +} + +extension LineAndBarChartData { + public func getHeaderLocation() -> InfoBoxPlacement { + return self.chartStyle.infoBoxPlacement + } +} +// MARK: - Line and Bar extension LineAndBarChartData where Self: LineChartDataProtocol { public func getYLabels() -> [Double] { var labels : [Double] = [Double]() @@ -127,81 +302,66 @@ extension LineAndBarChartData where Set: MultiDataSet { } } -public protocol LineChartDataProtocol: LineAndBarChartData { - associatedtype Style : CTLineChartStyle - var chartStyle : Style { get set } -} -public protocol BarChartDataProtocol: LineAndBarChartData { - associatedtype Style : CTBarChartStyle - var chartStyle : Style { get set } -} -public protocol PieChartDataProtocol : ChartData { - associatedtype Style : CTPieChartStyle - var chartStyle : Style { get set } -} - -// MARK: - Data Sets -public protocol DataSet: Hashable, Identifiable { - var id : ID { get } -} -public protocol SingleDataSet: DataSet { - associatedtype Styling : CTColourStyle - associatedtype DataPoint : CTChartDataPoint - - var dataPoints : [DataPoint] { get set } - var legendTitle : String { get set } - var pointStyle : PointStyle { get set } - var style : Styling { get set } -} -public protocol MultiDataSet: DataSet { - associatedtype DataSet : SingleDataSet - var dataSets : [DataSet] { get set } -} - - -// MARK: - Styles -public protocol CTChartStyle { - var infoBoxPlacement : InfoBoxPlacement { get set } - var globalAnimation : Animation { get set } -} -public protocol CTLineAndBarChartStyle: CTChartStyle { - var xAxisGridStyle : GridStyle { get set } - var yAxisGridStyle : GridStyle { get set } - var xAxisLabelPosition : XAxisLabelPosistion { get set } - var xAxisLabelsFrom : LabelsFrom { get set } - var yAxisLabelPosition : YAxisLabelPosistion { get set } - var yAxisNumberOfLabels : Int { get set } -} -public protocol CTLineChartStyle : CTLineAndBarChartStyle { - var baseline : Baseline { get set } -} -public protocol CTBarChartStyle : CTLineAndBarChartStyle { -} -public protocol CTPieChartStyle: CTChartStyle { -} - -public protocol CTColourStyle { - var colourType : ColourType { get set } - var colour : Color? { get set } - var colours : [Color]? { get set } - var stops : [GradientStop]? { get set } - var startPoint : UnitPoint? { get set } - var endPoint : UnitPoint? { get set } -} - -// MARK: - Data Points -public protocol CTChartDataPoint: Hashable, Identifiable { - var id : ID { get } - var value : Double { get set } - var pointDescription : String? { get set } - var date : Date? { get set } -} - -public protocol CTLineAndBarDataPoint: CTChartDataPoint { - var xAxisLabel : String? { get set } +// MARK: - Pie and Doughnut +extension PieAndDoughnutChartDataProtocol { + public func getHeaderLocation() -> InfoBoxPlacement { + return self.chartStyle.infoBoxPlacement + } } +extension PieAndDoughnutChartDataProtocol where Set == PieDataSet { -public protocol CTPieDataPoint: CTChartDataPoint { + 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 + } + } + + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [PieChartDataPoint] { + var points : [PieChartDataPoint] = [] + let touchDegree = degree(from: touchLocation, in: chartSize.frame(in: .local)) + + 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 { + points.append(data) + } + return points + } + + public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { + return [HashablePoint(x: touchLocation.x, y: touchLocation.y)] + } + public func setupLegends() { + for data in dataSets.dataPoints { + if let legend = data.pointDescription { + self.legends.append(LegendData(id : data.id, + legend : legend, + colour : data.colour, + strokeStyle: nil, + prioity : 1, + chartType : .pie)) + } + } + } + + func degree(from touchLocation: CGPoint, in rect: CGRect) -> CGFloat { + // http://www.cplusplus.com/reference/cmath/atan2/ + // https://stackoverflow.com/a/25398191 + 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 + } + } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift index fae210c0..8ca05e32 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift @@ -13,7 +13,7 @@ internal struct PointMarkers: ViewModifier where T: LineChartDataProtocol { private let minValue : Double private let range : Double - + internal init(chartData : T) { self.chartData = chartData @@ -34,13 +34,13 @@ internal struct PointMarkers: ViewModifier where T: LineChartDataProtocol { if chartData.chartType.dataSetType == .single { let data = chartData as! LineChartData - PointsSubView(dataSets: data.dataSets, minValue: minValue, range: range) - + PointsSubView(dataSets: data.dataSets, minValue: minValue, range: range, animation: chartData.chartStyle.globalAnimation, isFilled: chartData.isFilled) + } else if chartData.chartType.dataSetType == .multi { let data = chartData as! MultiLineChartData ForEach(data.dataSets.dataSets, id: \.self) { dataSet in - PointsSubView(dataSets: dataSet, minValue: minValue, range: range) + PointsSubView(dataSets: dataSet, minValue: minValue, range: range, animation: chartData.chartStyle.globalAnimation, isFilled: chartData.isFilled) } } } @@ -61,29 +61,76 @@ struct PointsSubView: View { let dataSets: LineDataSet let minValue : Double let range : Double + let animation: Animation + let isFilled : Bool + + @State var startAnimation : Bool = false var body: some View { switch dataSets.pointStyle.pointType { case .filled: + Point(dataSet : dataSets, minValue : minValue, range : range) - .fill(dataSets.pointStyle.fillColour) + .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) - .stroke(dataSets.pointStyle.borderColour, lineWidth: dataSets.pointStyle.lineWidth) + .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) - .stroke(dataSets.pointStyle.borderColour, lineWidth: dataSets.pointStyle.lineWidth) + .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/Shared/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift index 81936ef0..af3139de 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift @@ -33,10 +33,34 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { self.lineColour = lineColour self.strokeStyle = strokeStyle - self.markerValue = isAverage ? chartData.getAverage() : markerValue - self.range = chartData.getRange() - self.minValue = chartData.getMinValue() - self.maxValue = chartData.getMaxValue() + if chartData.chartType.chartType == .line { + + let chartData = chartData as! LineChartData + + switch chartData.chartStyle.baseline { + case .minimumValue: + + self.markerValue = isAverage ? chartData.getAverage() : markerValue + self.range = chartData.getRange() + self.minValue = chartData.getMinValue() + self.maxValue = chartData.getMaxValue() + + case .zero: + + self.markerValue = isAverage ? chartData.getAverage() : markerValue + self.range = chartData.getRange() + self.minValue = 0 + self.maxValue = chartData.getMaxValue() + + } + + } else { + self.markerValue = isAverage ? chartData.getAverage() : markerValue + self.range = chartData.getRange() + self.minValue = chartData.getMinValue() + self.maxValue = chartData.getMaxValue() + } + } From c4abcafe6abd40e1a7775cf67496306bc8e2428a Mon Sep 17 00:00:00 2001 From: Will Dale Date: Tue, 2 Feb 2021 17:10:04 +0000 Subject: [PATCH 018/152] Add documentation to protocols. Split protocols up into separate files. --- .../BarChart/Models/BarChartProtocols.swift | 48 +++ .../LineChart/Models/LineChartData.swift | 4 +- .../LineChart/Models/LineChartProtocols.swift | 62 +++ .../LineChart/Models/LineChartStyle.swift | 21 +- .../Models/Doughnut/DoughnutChartData.swift | 42 ++ .../DoughnutChartStyle.swift} | 25 +- .../PieChart/Models/Pie/PieChartData.swift | 44 ++ .../PieChart/Models/Pie/PieChartStyle.swift | 31 ++ .../PieChart/Models/PieChartData.swift | 85 ---- .../PieChart/Models/PieChartProtocols.swift | 171 ++++++++ .../PieChart/Views/DoughnutChart.swift | 2 +- .../Shared/Extras/Calculations.swift | 4 +- .../SwiftUICharts/Shared/Extras/Enums.swift | 18 +- .../Shared/Models/ChartMetadata.swift | 2 + .../Shared/Models/ChartViewData.swift | 6 + .../Shared/Models/GradientStop.swift | 5 +- .../Shared/Models/GridStyle.swift | 1 + .../LineAndBar/LineAndBarProtocols.swift | 186 ++++++++ .../Shared/Models/Protocols.swift | 406 +++++++----------- .../Shared/ViewModifiers/PointMarkers.swift | 2 + 20 files changed, 788 insertions(+), 377 deletions(-) create mode 100644 Sources/SwiftUICharts/BarChart/Models/BarChartProtocols.swift create mode 100644 Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift create mode 100644 Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift rename Sources/SwiftUICharts/PieChart/Models/{PieChartStyle.swift => Doughnut/DoughnutChartStyle.swift} (53%) create mode 100644 Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift create mode 100644 Sources/SwiftUICharts/PieChart/Models/Pie/PieChartStyle.swift delete mode 100644 Sources/SwiftUICharts/PieChart/Models/PieChartData.swift create mode 100644 Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift create mode 100644 Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartProtocols.swift new file mode 100644 index 00000000..c79cd88f --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartProtocols.swift @@ -0,0 +1,48 @@ +// +// BarChartProtocols.swift +// +// +// Created by Will Dale on 02/02/2021. +// + +import SwiftUI + + +// MARK: - Chart Data +/** + A protocol to extend functionality of `LineAndBarChartData` specifically for Bar Charts. + + # Reference + [See LineAndBarChartData](x-source-tag://LineAndBarChartData) + + `LineAndBarChartData` conforms to [ChartData](x-source-tag://ChartData) + + - Tag: BarChartDataProtocol + */ +public protocol BarChartDataProtocol: LineAndBarChartData where CTStyle: CTBarChartStyle { + var chartStyle : CTStyle { get set } +} + + +extension LineAndBarChartData where Self: BarChartDataProtocol { + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let maxValue: Double = self.getMaxValue() + for index in 0...self.chartStyle.yAxisNumberOfLabels { + labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) + } + return labels + } +} + + +// MARK: - Style +/** + A protocol to extend functionality of `CTLineAndBarChartStyle` specifically for Bar Charts. + + Currently empty. + + - Tag: CTBarChartStyle + */ +public protocol CTBarChartStyle : CTLineAndBarChartStyle {} + diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index eaf2c072..8e6c72e2 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -7,7 +7,6 @@ import SwiftUI -/// The central model from which the chart is drawn. public class LineChartData: LineChartDataProtocol { public let id : UUID = UUID() @@ -15,12 +14,10 @@ public class LineChartData: LineChartDataProtocol { @Published public var dataSets : LineDataSet @Published public var metadata : ChartMetadata? @Published public var xAxisLabels : [String]? - /// Data model conatining the style data for the chart. @Published public var chartStyle : LineChartStyle @Published public var legends : [LegendData] @Published public var viewData : ChartViewData @Published public var isFilled : Bool = false - @Published public var infoView : InfoViewData = InfoViewData() public var noDataText : Text = Text("No Data") @@ -62,6 +59,7 @@ public class LineChartData: LineChartDataProtocol { } // MARK: Labels + // TODO --- Add from xaxis labels public func getXAxidLabels() -> some View { HStack(spacing: 0) { ForEach(dataSets.dataPoints) { data in diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift new file mode 100644 index 00000000..94e52a3a --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift @@ -0,0 +1,62 @@ +// +// LineChartProtocols.swift +// +// +// Created by Will Dale on 02/02/2021. +// + +import SwiftUI + +/** + A protocol to extend functionality of `LineAndBarChartData` specifically for Line Charts. + + # Reference + [See LineAndBarChartData](x-source-tag://LineAndBarChartData) + + `LineAndBarChartData` conforms to [ChartData](x-source-tag://ChartData) + + - Tag: LineChartDataProtocol + */ +public protocol LineChartDataProtocol: LineAndBarChartData where CTStyle: CTLineChartStyle { + var chartStyle : CTStyle { get set } + var isFilled : Bool { get set} +} + +extension LineAndBarChartData where Self: LineChartDataProtocol { + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let dataRange : Double + let minValue : Double + let range : Double + + switch self.chartStyle.baseline { + case .minimumValue: + minValue = self.getMinValue() + dataRange = self.getRange() + range = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) + case .zero: + minValue = 0 + dataRange = self.getMaxValue() + range = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) + } + labels.append(minValue) + for index in 1...self.chartStyle.yAxisNumberOfLabels { + labels.append(minValue + range * Double(index)) + } + return labels + } +} + +/** + A protocol to extend functionality of `CTLineAndBarChartStyle` specifically for Line Charts. + + - Tag: CTLineChartStyle + */ +public protocol CTLineChartStyle : CTLineAndBarChartStyle { + /** + Where to start drawing the line chart from. Zero or data set minium. + + [See Baseline](x-source-tag://Baseline) + */ + var baseline: Baseline { get set } +} diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift index f249f734..3c6d04a5 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift @@ -10,28 +10,22 @@ import SwiftUI /// Model for controlling the overall aesthetic of the chart. public struct LineChartStyle: CTLineChartStyle { - /// 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 globalAnimation : Animation + 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 public var baseline : Baseline - /// Gobal control of animations. - public var globalAnimation : Animation + + /// Model for controlling the overall aesthetic of the chart. /// - Parameters: @@ -68,8 +62,3 @@ public struct LineChartStyle: CTLineChartStyle { self.globalAnimation = globalAnimation } } - -public enum Baseline { - case minimumValue - case zero -} diff --git a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift new file mode 100644 index 00000000..a0f92bb0 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift @@ -0,0 +1,42 @@ +// +// DoughnutChartData.swift +// +// +// Created by Will Dale on 02/02/2021. +// + +import SwiftUI + +public class DoughnutChartData: DoughnutChartDataProtocol { + + @Published public var id : UUID = UUID() + @Published public var dataSets : PieDataSet + @Published public var metadata : ChartMetadata? + @Published public var chartStyle : DoughnutChartStyle + @Published public var legends : [LegendData] + @Published public var infoView : InfoViewData + + public var noDataText: Text + public var chartType: (chartType: ChartType, dataSetType: DataSetType) + + public init(dataSets : PieDataSet, + metadata : ChartMetadata? = nil, + 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 typealias Set = PieDataSet + public typealias DataPoint = PieChartDataPoint +} diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartStyle.swift similarity index 53% rename from Sources/SwiftUICharts/PieChart/Models/PieChartStyle.swift rename to Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartStyle.swift index 87a5075f..1efffde2 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartStyle.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartStyle.swift @@ -1,33 +1,12 @@ // -// PieChartStyle.swift +// DoughnutChartStyle.swift // // -// Created by Will Dale on 25/01/2021. +// Created by Will Dale on 02/02/2021. // import SwiftUI -/// Model for controlling the overall aesthetic of the chart. -public struct PieChartStyle: CTPieChartStyle { - - /// Placement of the information box that appears on touch input. - public var infoBoxPlacement : InfoBoxPlacement - - /// 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. - /// - globalAnimation: Gobal control of animations. - public init(infoBoxPlacement : InfoBoxPlacement = .floating, - globalAnimation : Animation = Animation.linear(duration: 1) - ) { - self.infoBoxPlacement = infoBoxPlacement - self.globalAnimation = globalAnimation - } -} - /// Model for controlling the overall aesthetic of the chart. public struct DoughnutChartStyle: CTDoughnutChartStyle { diff --git a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift new file mode 100644 index 00000000..082c4983 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift @@ -0,0 +1,44 @@ +// +// PieChartData.swift +// +// +// Created by Will Dale on 24/01/2021. +// + +import SwiftUI + +public class PieChartData: PieChartDataProtocol { + + @Published public var id : UUID = UUID() + @Published public var dataSets : PieDataSet + @Published public var metadata : ChartMetadata? + @Published public var chartStyle : PieChartStyle + @Published public var legends : [LegendData] + @Published public var infoView : InfoViewData + + public var noDataText: Text + public var chartType: (chartType: ChartType, dataSetType: DataSetType) + + public init(dataSets : PieDataSet, + metadata : ChartMetadata? = nil, + chartStyle : PieChartStyle = PieChartStyle(), + 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 typealias Set = PieDataSet + public typealias DataPoint = PieChartDataPoint +} + diff --git a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartStyle.swift new file mode 100644 index 00000000..efff3e3b --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartStyle.swift @@ -0,0 +1,31 @@ +// +// 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 { + + /// Placement of the information box that appears on touch input. + public var infoBoxPlacement : InfoBoxPlacement + + /// 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. + /// - globalAnimation: Gobal control of animations. + public init(infoBoxPlacement : InfoBoxPlacement = .floating, + globalAnimation : Animation = Animation.linear(duration: 1) + ) { + self.infoBoxPlacement = infoBoxPlacement + self.globalAnimation = globalAnimation + } +} + + diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift deleted file mode 100644 index d1ebfdfb..00000000 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartData.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// PieChartData.swift -// -// -// Created by Will Dale on 24/01/2021. -// - -import SwiftUI - -public class PieChartData: PieChartDataProtocol { - - @Published public var id : UUID = UUID() - @Published public var dataSets : PieDataSet - @Published public var metadata : ChartMetadata? - @Published public var xAxisLabels : [String]? - @Published public var chartStyle : PieChartStyle - @Published public var legends : [LegendData] - @Published public var infoView : InfoViewData - - public var noDataText: Text - public var chartType: (chartType: ChartType, dataSetType: DataSetType) - - public init(dataSets : PieDataSet, - metadata : ChartMetadata? = nil, - xAxisLabels : [String]? = nil, - chartStyle : PieChartStyle = PieChartStyle(), - noDataText : Text - ) { - self.dataSets = dataSets - self.metadata = metadata - self.xAxisLabels = xAxisLabels - self.chartStyle = chartStyle - self.legends = [LegendData]() - self.infoView = InfoViewData() - self.noDataText = noDataText - self.chartType = (chartType: .pie, dataSetType: .single) - - self.setupLegends() - - self.makeDataPoints() - } - - - public typealias Set = PieDataSet - public typealias DataPoint = PieChartDataPoint -} - -public class DoughnutChartData: DoughnutChartDataProtocol { - - @Published public var id : UUID = UUID() - @Published public var dataSets : PieDataSet - @Published public var metadata : ChartMetadata? - @Published public var xAxisLabels : [String]? - @Published public var chartStyle : DoughnutChartStyle - @Published public var legends : [LegendData] - @Published public var infoView : InfoViewData - - let strokeWidth: CGFloat = 30 - - public var noDataText: Text - public var chartType: (chartType: ChartType, dataSetType: DataSetType) - - public init(dataSets : PieDataSet, - metadata : ChartMetadata? = nil, - xAxisLabels : [String]? = nil, - chartStyle : DoughnutChartStyle = DoughnutChartStyle(), - noDataText : Text - ) { - self.dataSets = dataSets - self.metadata = metadata - self.xAxisLabels = xAxisLabels - self.chartStyle = chartStyle - self.legends = [LegendData]() - self.infoView = InfoViewData() - self.noDataText = noDataText - self.chartType = (chartType: .pie, dataSetType: .single) - - self.setupLegends() - - self.makeDataPoints() - } - - public typealias Set = PieDataSet - public typealias DataPoint = PieChartDataPoint -} diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift new file mode 100644 index 00000000..a3ef63ae --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift @@ -0,0 +1,171 @@ +// +// PieChartProtocols.swift +// +// +// Created by Will Dale on 02/02/2021. +// + +import SwiftUI + + +// MARK: - Chart Data +/** + A protocol to extend functionality of `ChartData` specifically for Pie and Doughnut Charts. + + # Reference + [See ChartData](x-source-tag://ChartData) + + - Tag: PieAndDoughnutChartDataProtocol + */ +public protocol PieAndDoughnutChartDataProtocol: ChartData { + associatedtype CTStyle : CTPieAndDoughnutChartStyle + + /** + Protocol to set the styling data for the chart. + */ + var chartStyle : CTStyle { get set } +} + +/** + A protocol to extend functionality of `PieAndDoughnutChartDataProtocol` specifically for Pie Charts. + + # Reference + [See PieAndDoughnutChartDataProtocol](x-source-tag://PieAndDoughnutChartDataProtocol) + + - Tag: PieChartDataProtocol + */ +public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol where CTStyle: CTPieChartStyle { + + /** + Protocol to set the styling data for the chart. + */ + var chartStyle : CTStyle { get set } +} + +/** + A protocol to extend functionality of `PieAndDoughnutChartDataProtocol` specifically for Doughnut Charts. + + # Reference + [See DoughnutChartDataProtocol](x-source-tag://DoughnutChartDataProtocol) + + - Tag: DoughnutChartDataProtocol + */ +public protocol DoughnutChartDataProtocol : PieAndDoughnutChartDataProtocol where CTStyle: CTDoughnutChartStyle { + + /** + Protocol to set the styling data for the chart. + */ + var chartStyle : CTStyle { get set } +} + + +// MARK: - Pie and Doughnut +extension PieAndDoughnutChartDataProtocol { + public func getHeaderLocation() -> InfoBoxPlacement { + return self.chartStyle.infoBoxPlacement + } +} +extension PieAndDoughnutChartDataProtocol where Set == PieDataSet { + + 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 + } + } + + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [PieChartDataPoint] { + var points : [PieChartDataPoint] = [] + let touchDegree = degree(from: touchLocation, in: chartSize.frame(in: .local)) + + 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 { + points.append(data) + } + return points + } + + public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { + return [HashablePoint(x: touchLocation.x, y: touchLocation.y)] + } + + public func setupLegends() { + for data in dataSets.dataPoints { + if let legend = data.pointDescription { + self.legends.append(LegendData(id : data.id, + legend : legend, + colour : data.colour, + strokeStyle: nil, + prioity : 1, + chartType : .pie)) + } + } + } + + func degree(from touchLocation: CGPoint, in rect: CGRect) -> CGFloat { + // http://www.cplusplus.com/reference/cmath/atan2/ + // https://stackoverflow.com/a/25398191 + 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 + } + } +} + +// MARK: Style +/** + A protocol to extend functionality of `CTChartStyle` specifically for Pie and Doughnut Charts. + + Currently empty. + + - Tag: CTPieAndDoughnutChartStyle + */ +public protocol CTPieAndDoughnutChartStyle: CTChartStyle {} + + +/** + A protocol to extend functionality of `CTPieAndDoughnutChartStyle` specifically for Pie Charts. + + Currently empty. + + - Tag: CTPieChartStyle + */ +public protocol CTPieChartStyle: CTPieAndDoughnutChartStyle {} + + +/** + A protocol to extend functionality of `CTPieAndDoughnutChartStyle` specifically for Doughnut Charts. + + - Tag: CTDoughnutChartStyle + */ +public protocol CTDoughnutChartStyle: CTPieAndDoughnutChartStyle { + + /** + Width / Delta of the Doughnut Chart + */ + var strokeWidth: CGFloat { get set } +} + + +// MARK: DataPoints + +/** + A protocol to extend functionality of `CTChartDataPoint` specifically for Pie and Doughnut Charts. + + Currently empty. + + - Tag: CTPieDataPoint + */ +public protocol CTPieDataPoint: CTChartDataPoint {} + + diff --git a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift index 679e9f44..16398ace 100644 --- a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift @@ -24,7 +24,7 @@ public struct DoughnutChart: View where ChartData: DoughnutChartData DoughnutSegmentShape(id: chartData.dataSets.dataPoints[data].id, startAngle: chartData.dataSets.dataPoints[data].startAngle, amount: chartData.dataSets.dataPoints[data].amount) - .strokeBorder(chartData.dataSets.dataPoints[data].colour, lineWidth: chartData.strokeWidth) + .strokeBorder(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)) diff --git a/Sources/SwiftUICharts/Shared/Extras/Calculations.swift b/Sources/SwiftUICharts/Shared/Extras/Calculations.swift index 36604057..07f88c45 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Calculations.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Calculations.swift @@ -6,7 +6,9 @@ // import SwiftUI - +/** + - Tag: Calculations + */ //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. diff --git a/Sources/SwiftUICharts/Shared/Extras/Enums.swift b/Sources/SwiftUICharts/Shared/Extras/Enums.swift index 4b079ca8..e76daf5b 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Enums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Enums.swift @@ -67,7 +67,7 @@ public enum ChartType { // MARK: - Style /** - Type of colour styling for the chart. + Type of colour styling. ``` case colour // Single Colour case gradientColour // Colour Gradient @@ -102,6 +102,22 @@ public enum LineType { case curvedLine } +/** + Where to start drawing the line chart from. + ``` + case minimumValue // Lowest value in the data set(s) + case zero // Set 0 as the lowest value + ``` + + - Tag: Baseline + */ +public enum Baseline { + /// Lowest value in the data set(s) + case minimumValue + /// Set 0 as the lowest value + case zero +} + // MARK: - BarStyle /** Where to get the colour data from. diff --git a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift index 207cd748..ed801e4b 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift @@ -8,6 +8,8 @@ import Foundation /// Data model for the chart's metadata +/// +/// - Tag: ChartMetadata public struct ChartMetadata { /// The charts Title var title : String? diff --git a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift index a1eb3fae..a03b905c 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift @@ -8,6 +8,7 @@ import Foundation /// Data model to pass view information internally so the layout can configure its self. +/// - Tag: ChartViewData public struct ChartViewData { /// If the chart has labels on the X axis, the Y axis needs a different layout @@ -19,6 +20,11 @@ public struct ChartViewData { } /// Data model to pass view information internally so the layout can configure its self. +/// +/// # Reference +/// [CTChartDataPoint](x-source-tag://CTChartDataPoint) +/// +/// - Tag: InfoViewData public struct InfoViewData { /** Is there currently input (touch or click) on the chart diff --git a/Sources/SwiftUICharts/Shared/Models/GradientStop.swift b/Sources/SwiftUICharts/Shared/Models/GradientStop.swift index c7e8e1f7..ed12b300 100644 --- a/Sources/SwiftUICharts/Shared/Models/GradientStop.swift +++ b/Sources/SwiftUICharts/Shared/Models/GradientStop.swift @@ -20,7 +20,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 +34,3 @@ public struct GradientStop: Hashable { return stopsArray } } - diff --git a/Sources/SwiftUICharts/Shared/Models/GridStyle.swift b/Sources/SwiftUICharts/Shared/Models/GridStyle.swift index 32be3bd7..d4fffe03 100644 --- a/Sources/SwiftUICharts/Shared/Models/GridStyle.swift +++ b/Sources/SwiftUICharts/Shared/Models/GridStyle.swift @@ -8,6 +8,7 @@ import SwiftUI /// Model for controlling the look of the Grid +/// - Tag: GridStyle public struct GridStyle { /// Number of lines to break up the axis diff --git a/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift b/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift new file mode 100644 index 00000000..2be40f05 --- /dev/null +++ b/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift @@ -0,0 +1,186 @@ +// +// LineAndBarProtocols.swift +// +// +// Created by Will Dale on 02/02/2021. +// + +import SwiftUI + +// MARK: - Chart Data +/** + A protocol to extend functionality of `ChartData` specifically for Line and Bar Charts. + + # Reference + [See ChartData](x-source-tag://ChartData) + + - Tag: LineAndBarChartData + */ +public protocol LineAndBarChartData : ChartData { + + associatedtype Body : View + associatedtype CTStyle : CTLineAndBarChartStyle + + /** + 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 } + /** + Data model conatining the style data for the chart. + + # Reference + [CTChartStyle](x-source-tag://CTChartStyle) + */ + var chartStyle: CTStyle { get set } + + /** + Array of strings for the labels on the X Axis instead of the labels in the data points. + + To control where the labels should come from. Set [LabelsFrom](x-source-tag://LabelsFrom) in [ChartStyle](x-source-tag://CTChartStyle). + */ + var xAxisLabels: [String]? { get set } + + /** + Displays a view for the labels on the X Axis. + + Labels can come from either [CTChartDataPoint](x-source-tag://CTChartDataPoint) or [ChartData](x-source-tag://ChartData) + + - Returns: An `HStack` of `Text` containin x axis labels. + + - Tag: getXAxidLabels + */ + func getXAxidLabels() -> Body + + /** + 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. + + - Tag: getYLabels + */ + func getYLabels() -> [Double] + + /** + Returns the difference between the highest and lowest numbers in the data set or data sets. + - Tag: getRange + */ + func getRange() -> Double + + /** + Returns the lowest value in the data set or data sets. + - Tag: getMinValue + */ + func getMinValue() -> Double + + /** + Returns the highest value in the data set or data sets + - Tag: getMaxValue + */ + func getMaxValue() -> Double + + /** + Returns the average value from the data set or data sets. + - Tag: getAverage + */ + func getAverage() -> Double +} + +extension LineAndBarChartData { + public func getHeaderLocation() -> InfoBoxPlacement { + return self.chartStyle.infoBoxPlacement + } +} +extension LineAndBarChartData where Set: SingleDataSet { + public func getRange() -> Double { + DataFunctions.dataSetRange(from: dataSets) + } + public func getMinValue() -> Double { + DataFunctions.dataSetMinValue(from: dataSets) + } + public func getMaxValue() -> Double { + DataFunctions.dataSetMaxValue(from: dataSets) + } + public func getAverage() -> Double { + DataFunctions.dataSetAverage(from: dataSets) + } +} +extension LineAndBarChartData where Set: MultiDataSet { + public func getRange() -> Double { + DataFunctions.multiDataSetRange(from: dataSets) + } + public func getMinValue() -> Double { + DataFunctions.multiDataSetMinValue(from: dataSets) + } + public func getMaxValue() -> Double { + DataFunctions.multiDataSetMaxValue(from: dataSets) + } + public func getAverage() -> Double { + DataFunctions.multiDataSetAverage(from: dataSets) + } +} + +// MARK: - Style +/** + A protocol to extend functionality of `CTChartStyle` specifically for Line and Bar Charts. + + - Tag: CTLineAndBarChartStyle + */ +public protocol CTLineAndBarChartStyle: CTChartStyle { + /** + Style of the vertical lines breaking up the chart + + [See GridStyle](x-source-tag://GridStyle) + */ + var xAxisGridStyle: GridStyle { get set } + + /** + Style of the horizontal lines breaking up the chart. + + [See GridStyle](x-source-tag://GridStyle) + */ + var yAxisGridStyle: GridStyle { get set } + + /** + Location of the X axis labels - Top or Bottom + + [See XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) + */ + var xAxisLabelPosition: XAxisLabelPosistion { get set } + + /** + Where the label data come from. DataPoint or ChartData + + [See LabelsFrom](x-source-tag://LabelsFrom) + */ + var xAxisLabelsFrom: LabelsFrom { get set } + + /** + Location of the X axis labels - Leading or Trailing + + [See YAxisLabelPosistion](x-source-tag://YAxisLabelPosistion) + */ + var yAxisLabelPosition: YAxisLabelPosistion { get set } + + /** + Number Of Labels on Y Axis + */ + var yAxisNumberOfLabels: Int { get set } +} + +// MARK: DataPoints +/** + A protocol to extend functionality of `CTChartDataPoint` specifically for Line and Bar Charts. + + - Tag: CTLineAndBarDataPoint + */ +public protocol CTLineAndBarDataPoint: CTChartDataPoint { + + /// Data points label for the X axis. + var xAxisLabel : String? { get set } +} + + diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols.swift index d87ae4a3..9184e418 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols.swift @@ -7,36 +7,50 @@ import SwiftUI -/// The main +/** + Main protocol for passing data around library. + + All Chart Data models ultimately conform to this. + + - Tag: ChartData + */ public protocol ChartData: ObservableObject, Identifiable { associatedtype Set : DataSet associatedtype DataPoint: CTChartDataPoint + var id: ID { get } - var id : UUID { get } - /// Data model containing the datapoints. - /// - Note: - /// `Set` is either `SingleData` or `MultiDataSet`. - var dataSets : Set { get set } + /** + Data model containing the datapoints. - /// Data model containing: the charts Title, the charts Subtitle and the Line Legend. - var metadata : ChartMetadata? { get set } + `Set` is either `SingleData` or `MultiDataSet`. + */ + var dataSets: Set { get set } - /// Array of strings for the labels on the X Axis instead of the labels in the data points. - /// - Note: - /// To control where the labels should come from; set `xAxisLabelsFrom` in `ChartStyle`. - var xAxisLabels : [String]? { get set } // Not Pie + /** + Data model containing: the charts Title, the charts Subtitle and the Line Legend. + + # Reference + [ChartType](x-source-tag://ChartType) + */ + var metadata: ChartMetadata? { get set } - /// Array of `LegendData` to populate the chart legend. - /// - Note: - /// This is populated automatically from within each view. - var legends : [LegendData] { get set } + /** + Array of `LegendData` to populate the chart legend. + + This is populated automatically from within each view. + */ + var legends: [LegendData] { get set } - /// Data model to hold temporary data from `TouchOverlay` ViewModifier and pass the data points to display in the `HeaderView`. - var infoView : InfoViewData { get set } + /** + Data model to hold temporary data from `TouchOverlay` ViewModifier and pass the data points to display in the `HeaderView`. + */ + var infoView: InfoViewData { get set } - /// Customisable `Text` to display when where is not enough data to draw the chart. - var noDataText : Text { get set } + /** + Customisable `Text` to display when where is not enough data to draw the chart. + */ + var noDataText: Text { get set } /** Holds metadata about the chart. @@ -49,8 +63,9 @@ public protocol ChartData: ObservableObject, Identifiable { [ChartType](x-source-tag://ChartType) [DataSetType](x-source-tag://DataSetType) + */ - var chartType : (chartType: ChartType, dataSetType: DataSetType) { get } + var chartType: (chartType: ChartType, dataSetType: DataSetType) { get } /** Sets the order the Legends are layed out in. @@ -58,6 +73,7 @@ public protocol ChartData: ObservableObject, Identifiable { # Reference [LegendData](x-source-tag://LegendData) + - Tag: legendOrder */ func legendOrder() -> [LegendData] @@ -95,71 +111,74 @@ public protocol ChartData: ObservableObject, Identifiable { */ func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] - /// Configures the legends based on the type of chart. + /** + Configures the legends based on the type of chart. + + - Tag: setupLegends + */ func setupLegends() } - - - -public protocol LineAndBarChartData : ChartData { - - associatedtype Body : View - associatedtype CTStyle : CTLineAndBarChartStyle - /// 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 } - var chartStyle : CTStyle { get set } - - func getXAxidLabels() -> Body - func getYLabels() -> [Double] - - func getRange() -> Double - func getMinValue() -> Double - func getMaxValue() -> Double - func getAverage() -> Double -} -public protocol LineChartDataProtocol: LineAndBarChartData where CTStyle: CTLineChartStyle { - var chartStyle : CTStyle { get set } - var isFilled : Bool { get set} -} -public protocol BarChartDataProtocol: LineAndBarChartData where CTStyle: CTBarChartStyle { - var chartStyle : CTStyle { get set } -} - - - - -public protocol PieAndDoughnutChartDataProtocol: ChartData { - associatedtype CTStyle : CTPieAndDoughnutChartStyle - var chartStyle : CTStyle { get set } -} - -public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol where CTStyle: CTPieChartStyle { - var chartStyle : CTStyle { get set } -} -public protocol DoughnutChartDataProtocol : PieAndDoughnutChartDataProtocol where CTStyle: CTDoughnutChartStyle { - var chartStyle : CTStyle { get set } +extension ChartData { + public func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } } - - // MARK: - Data Sets +/** + Main protocol set conformace for types of Data Sets. + + - Tag: DataSet + */ public protocol DataSet: Hashable, Identifiable { var id : ID { get } } + +/** + Protocol for data sets that only require a single set of data . + + - Tag: SingleDataSet + */ public protocol SingleDataSet: DataSet { associatedtype Styling : CTColourStyle associatedtype DataPoint : CTChartDataPoint + /** + Array of data points. [See CTChartDataPoint](x-source-tag://CTChartDataPoint) + */ var dataPoints : [DataPoint] { get set } + + /** + Label to display in the legend. + */ var legendTitle : String { get set } - var pointStyle : PointStyle { get set } + + /** + Sets the look of the markers over the data points. + + The markers are layed out when the `ViewModifier` [.pointMarkers](x-source-tag://PointMarkers) + is applied. + */ + var pointStyle : PointStyle { get set } // Line Only ---------------------------- + + /** + Sets the style for the Data Set (as opposed to Chart Data Style). + */ var style : Styling { get set } } + +/** + Protocol for data sets that require a multiple sets of data . + + - Tag: MultiDataSet + */ public protocol MultiDataSet: DataSet { associatedtype DataSet : SingleDataSet + /** + Array of DataSets. + [See SingleDataSet](x-source-tag://SingleDataSet) + */ var dataSets : [DataSet] { get set } } @@ -167,201 +186,98 @@ public protocol MultiDataSet: DataSet { // MARK: - Styles +/** + Protocol to set the styling data for the chart. + + - Tag: CTChartStyle + */ public protocol CTChartStyle { + /** + Placement of the information box that appears on touch input. + + # Reference + [See InfoBoxPlacement](x-source-tag://InfoBoxPlacement) + */ var infoBoxPlacement : InfoBoxPlacement { get set } + + /** + Global control of animations. + + ``` + Animation.linear(duration: 1) + ``` + */ var globalAnimation : Animation { get set } } -public protocol CTLineAndBarChartStyle: CTChartStyle { - var xAxisGridStyle : GridStyle { get set } - var yAxisGridStyle : GridStyle { get set } - var xAxisLabelPosition : XAxisLabelPosistion { get set } - var xAxisLabelsFrom : LabelsFrom { get set } - var yAxisLabelPosition : YAxisLabelPosistion { get set } - var yAxisNumberOfLabels : Int { get set } -} -public protocol CTLineChartStyle : CTLineAndBarChartStyle { - var baseline : Baseline { get set } -} -public protocol CTBarChartStyle : CTLineAndBarChartStyle {} - - -public protocol CTPieAndDoughnutChartStyle: CTChartStyle {} - -public protocol CTPieChartStyle: CTPieAndDoughnutChartStyle {} - -public protocol CTDoughnutChartStyle: CTPieAndDoughnutChartStyle { - var strokeWidth : CGFloat { get set } -} - +/** + A protocol to set varius colour styles. + + Allows for single colour, gradient or gradient with stops control. + + - Tag: CTDoughnutChartStyle + */ public protocol CTColourStyle { - var colourType : ColourType { get set } - var colour : Color? { get set } - var colours : [Color]? { get set } - var stops : [GradientStop]? { get set } - var startPoint : UnitPoint? { get set } - var endPoint : UnitPoint? { get set } + + /** + Selection for the style of colour. + + [See ColourType](x-source-tag://ColourType) + */ + 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 + + [See GradientStop](x-source-tag://GradientStop) + */ + 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 } } // MARK: - Data Points + +/** + Protocol to set base configuration for data points. + + - Tag: CTChartDataPoint + + */ public protocol CTChartDataPoint: Hashable, Identifiable { var id : ID { get } - var value : Double { get set } - var pointDescription : String? { get set } - var date : Date? { get set } -} -public protocol CTLineAndBarDataPoint: CTChartDataPoint { - var xAxisLabel : String? { get set } -} -public protocol CTPieDataPoint: CTChartDataPoint { -} - - - -// MARK: - Extensions - -extension ChartData { - public func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } -} - -extension LineAndBarChartData { - public func getHeaderLocation() -> InfoBoxPlacement { - return self.chartStyle.infoBoxPlacement - } -} - -// MARK: - Line and Bar -extension LineAndBarChartData where Self: LineChartDataProtocol { - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let dataRange : Double - let minValue : Double - let range : Double - - switch self.chartStyle.baseline { - case .minimumValue: - minValue = self.getMinValue() - dataRange = self.getRange() - range = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) - case .zero: - minValue = 0 - dataRange = self.getMaxValue() - range = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) - } - labels.append(minValue) - for index in 1...self.chartStyle.yAxisNumberOfLabels { - labels.append(minValue + range * Double(index)) - } - return labels - } -} -extension LineAndBarChartData where Self: BarChartDataProtocol { - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let maxValue: Double = self.getMaxValue() - for index in 0...self.chartStyle.yAxisNumberOfLabels { - labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) - } - return labels - } -} - -extension LineAndBarChartData where Set: SingleDataSet { - public func getRange() -> Double { - DataFunctions.dataSetRange(from: dataSets) - } - public func getMinValue() -> Double { - DataFunctions.dataSetMinValue(from: dataSets) - } - public func getMaxValue() -> Double { - DataFunctions.dataSetMaxValue(from: dataSets) - } - public func getAverage() -> Double { - DataFunctions.dataSetAverage(from: dataSets) - } -} -extension LineAndBarChartData where Set: MultiDataSet { - public func getRange() -> Double { - DataFunctions.multiDataSetRange(from: dataSets) - } - public func getMinValue() -> Double { - DataFunctions.multiDataSetMinValue(from: dataSets) - } - public func getMaxValue() -> Double { - DataFunctions.multiDataSetMaxValue(from: dataSets) - } - public func getAverage() -> Double { - DataFunctions.multiDataSetAverage(from: dataSets) - } -} - - -// MARK: - Pie and Doughnut -extension PieAndDoughnutChartDataProtocol { - public func getHeaderLocation() -> InfoBoxPlacement { - return self.chartStyle.infoBoxPlacement - } -} -extension PieAndDoughnutChartDataProtocol where Set == PieDataSet { - - 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 - } - } - - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [PieChartDataPoint] { - var points : [PieChartDataPoint] = [] - let touchDegree = degree(from: touchLocation, in: chartSize.frame(in: .local)) - - 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 { - points.append(data) - } - return points - } + /// Value of the data point + var value : Double { get set } - public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - return [HashablePoint(x: touchLocation.x, y: touchLocation.y)] - } + /** + A laabel that can be displayed on touch input - public func setupLegends() { - for data in dataSets.dataPoints { - if let legend = data.pointDescription { - self.legends.append(LegendData(id : data.id, - legend : legend, - colour : data.colour, - strokeStyle: nil, - prioity : 1, - chartType : .pie)) - } - } - } + It can eight be displayed in a floating box that tracks the users input location + or placed in the header. [See InfoBoxPlacement](x-source-tag://InfoBoxPlacement). + */ + var pointDescription : String? { get set } - func degree(from touchLocation: CGPoint, in rect: CGRect) -> CGFloat { - // http://www.cplusplus.com/reference/cmath/atan2/ - // https://stackoverflow.com/a/25398191 - 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 - } - } + /** + Date can be used for performing additional calculations. + + [See Calculations](x-source-tag://Calculations) + */ + var date : Date? { get set } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift index 8ca05e32..5601845a 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift @@ -7,6 +7,8 @@ import SwiftUI + +/// - Tag: PointMarkers internal struct PointMarkers: ViewModifier where T: LineChartDataProtocol { @ObservedObject var chartData: T From a4ac6260d805f9833e1c32c59c7cf39ec55b5c57 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 3 Feb 2021 16:12:45 +0000 Subject: [PATCH 019/152] Add documentation. --- .../LineChart/Models/LineChartData.swift | 173 +++++++++++++++++- .../LineChart/Models/LineChartDataPoint.swift | 26 ++- .../LineChart/Models/LineChartStyle.swift | 80 ++++++-- .../LineChart/Models/LineDataSet.swift | 92 +++++++++- .../LineChart/Models/LineStyle.swift | 77 ++++++-- .../LineChart/Models/MultiLineChartData.swift | 156 +++++++++++++++- .../LineChart/Views/LineChartView.swift | 85 +++++++++ .../SwiftUICharts/Shared/Extras/Enums.swift | 4 +- .../Shared/Models/ChartMetadata.swift | 19 +- .../Shared/Models/GridStyle.swift | 25 ++- .../Shared/Models/HashablePoint.swift | 22 +++ .../Shared/Models/PointStyle.swift | 40 +++- .../Shared/Models/Protocols.swift | 17 +- .../Shared/Shapes/TouchOverlayMarker.swift | 4 +- .../Shared/ViewModifiers/HeaderBox.swift | 15 +- .../Shared/ViewModifiers/Legends.swift | 10 +- .../Shared/ViewModifiers/PointMarkers.swift | 57 ++++-- .../Shared/ViewModifiers/TouchOverlay.swift | 47 +++-- .../Shared/ViewModifiers/XAxisGrid.swift | 20 ++ .../Shared/ViewModifiers/XAxisLabels.swift | 25 ++- .../Shared/ViewModifiers/YAxisGrid.swift | 24 ++- .../Shared/ViewModifiers/YAxisLabels.swift | 17 ++ .../Shared/ViewModifiers/YAxisPOI.swift | 109 ++++++++--- 23 files changed, 1027 insertions(+), 117 deletions(-) create mode 100644 Sources/SwiftUICharts/Shared/Models/HashablePoint.swift diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index 8e6c72e2..9c19a364 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -7,6 +7,148 @@ import SwiftUI +/** + Data for drawing and styling a single line, line chart. + + This model contains all the data and styling information for a single line, line chart. + + # Example + ``` + static func makeData() -> LineChartData { + + let data = LineDataSet(dataPoints: [ + LineChartDataPoint(value: 20, xAxisLabel: "M", pointLabel: "Monday"), + LineChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"), + LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), + LineChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"), + LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), + LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), + LineChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") + ], + legendTitle: "Data", + pointStyle: PointStyle(), + style: LineStyle()) + + let metadata = ChartMetadata(title: "Some Data", subtitle: "A Week") + + let labels = ["Monday", "Thursday", "Sunday"] + + return LineChartData(dataSets: data, + metadata: metadata, + xAxisLabels: labels, + chartStyle: LineChartStyle(), + calculations: .none) + } + + ``` + + --- + + # Parts + + ## LineDataSet + ``` + LineDataSet(dataPoints: [LineChartDataPoint], + legendTitle: String, + pointStyle: PointStyle, + style: LineStyle) + ``` + ### LineChartDataPoint + ``` + LineChartDataPoint(value: Double, + xAxisLabel: String?, + pointLabel: String?, + date: Date?) + ``` + + ### PointStyle + ``` + PointStyle(pointSize: CGFloat, + borderColour: Color, + fillColour: Color, + lineWidth: CGFloat, + pointType: PointType, + pointShape: PointShape) + ``` + + ### LineStyle + ``` + LineStyle(colour: Color, + ...) + + LineStyle(colours: [Color], + startPoint: UnitPoint, + endPoint: UnitPoint, + ...) + + LineStyle(stops: [GradientStop], + startPoint: UnitPoint, + endPoint: UnitPoint, + ...) + + LineStyle(..., + lineType: LineType, + strokeStyle: Stroke, + ignoreZero: Bool) + ``` + + ## ChartMetadata + ``` + ChartMetadata(title: String?, subtitle: String?) + ``` + + ## LineChartStyle + + ``` + LineChartStyle(infoBoxPlacement : InfoBoxPlacement, + xAxisGridStyle : GridStyle, + yAxisGridStyle : GridStyle, + xAxisLabelPosition : XAxisLabelPosistion, + xAxisLabelsFrom : LabelsFrom, + yAxisLabelPosition : YAxisLabelPosistion, + yAxisNumberOfLabels : Int, + baseline : Baseline, + globalAnimation : Animation) + ``` + + ### GridStyle + ``` + GridStyle(numberOfLines: Int, + lineColour : Color, + lineWidth : CGFloat, + dash : [CGFloat], + dashPhase : CGFloat) + ``` + + --- + + # Also See + - [Line Data Set](x-source-tag://LineDataSet) + - [Line Chart Data Point](x-source-tag://LineChartDataPoint) + - [Point Style](x-source-tag://PointStyle) + - [PointType](x-source-tag://PointType) + - [PointShape](x-source-tag://PointShape) + - [Line Style](x-source-tag://LineStyle) + - [ColourType](x-source-tag://ColourType) + - [LineType](x-source-tag://LineType) + - [GradientStop](x-source-tag://GradientStop) + - [Chart Metadata](x-source-tag://ChartMetadata) + - [Line Chart Style](x-source-tag://LineChartStyle) + - [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) + - [GridStyle](x-source-tag://GridStyle) + - [XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) + - [LabelsFrom](x-source-tag://LabelsFrom) + - [YAxisLabelPosistion](x-source-tag://YAxisLabelPosistion) + + # Conforms to + - ObservableObject + - Identifiable + - LineChartDataProtocol + - LineAndBarChartData + - ChartData + + - Tag: LineChartData + */ public class LineChartData: LineChartDataProtocol { public let id : UUID = UUID() @@ -21,9 +163,21 @@ public class LineChartData: LineChartDataProtocol { @Published public var infoView : InfoViewData = InfoViewData() public var noDataText : Text = Text("No Data") - public var chartType : (chartType: ChartType, dataSetType: DataSetType) - + + /// Initialises a Single Line Chart with optional calculation + /// + /// Has the option perform optional calculation on the data set, such as averaging based on date. + /// + /// - Note: + /// To add custom calculations use the initialiser with `customCalc`. + /// + /// - 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. + /// - calculations: Addition calculations that can be performed on the data set before drawing. public init(dataSets : LineDataSet, metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, @@ -37,10 +191,22 @@ public class LineChartData: LineChartDataProtocol { self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (chartType: .line, dataSetType: .single) - self.setupLegends() } + /// Initializes a Single Line Chart with custom calculation + /// + /// Has the option perform custom calculations on the data set. + /// + /// - Note: + /// To add pre built calculations use the initialiser with `calculations`. + /// + /// - Parameters: + /// - dataSets: Data to draw 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. + /// - customCalc: Custom calculations that can be performed on the data set before drawing. public init(dataSets : LineDataSet, metadata : ChartMetadata? = nil, xAxisLabels : [String]? = nil, @@ -54,7 +220,6 @@ public class LineChartData: LineChartDataProtocol { self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (chartType: .line, dataSetType: .single) - self.setupLegends() } diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift index ec02588d..21c5c33f 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift @@ -7,21 +7,35 @@ import SwiftUI -/// Data model for a data point. +/** + Data for a single data point. + + # Example + ``` + LineChartDataPoint(value: 20, + xAxisLabel: "M", + pointLabel: "Monday", + data: Date()) + ``` + + # Conforms to + - CTLineAndBarDataPoint + - CTChartDataPoint + - Hashable + - Identifiable + + - Tag: LineChartDataPoint + */ public struct LineChartDataPoint: CTLineAndBarDataPoint { 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? - /// Data model for a single data point with colour for use with a bar chart. + /// 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. diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift index 3c6d04a5..10f86d58 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift @@ -7,25 +7,75 @@ import SwiftUI -/// Model for controlling the overall aesthetic of the chart. +/** + 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), + + # Example + ``` + LineChartStyle(infoBoxPlacement: .header, + xAxisGridStyle : GridStyle(numberOfLines: 7, + lineColour : .gray, + lineWidth : 1, + dash : [8], + dashPhase : 0), + yAxisGridStyle : GridStyle(numberOfLines: 7, + lineColour : .gray, + lineWidth : 1, + dash : [8], + dashPhase : 0), + xAxisLabelPosition : .bottom, + xAxisLabelsFrom : .dataPoint, + yAxisLabelPosition : .leading, + yAxisNumberOfLabels : 5, + baseline : .minimumValue, + globalAnimation : .linear(duration: 1)) + ``` + + # Options + ``` + LineChartStyle(infoBoxPlacement : InfoBoxPlacement, + xAxisGridStyle : GridStyle, + yAxisGridStyle : GridStyle, + xAxisLabelPosition : XAxisLabelPosistion, + xAxisLabelsFrom : LabelsFrom, + yAxisLabelPosition : YAxisLabelPosistion, + yAxisNumberOfLabels : Int, + baseline : Baseline, + globalAnimation : Animation) + ``` + + --- + + # Also See + - [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) + - [GridStyle](x-source-tag://GridStyle) + - [XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) + - [LabelsFrom](x-source-tag://LabelsFrom) + - [YAxisLabelPosistion](x-source-tag://YAxisLabelPosistion) + + # Conforms to + - CTLineChartStyle + - CTLineAndBarChartStyle + - CTChartStyle + + - Tag: LineChartStyle + */ public struct LineChartStyle: CTLineChartStyle { - - - public var infoBoxPlacement : InfoBoxPlacement - public var globalAnimation : Animation - - public var xAxisGridStyle : GridStyle - public var yAxisGridStyle : GridStyle - - public var xAxisLabelPosition: XAxisLabelPosistion - public var xAxisLabelsFrom : LabelsFrom - public var yAxisLabelPosition : YAxisLabelPosistion - public var yAxisNumberOfLabels : Int - - public var baseline : Baseline + public var infoBoxPlacement : InfoBoxPlacement + public var globalAnimation : Animation + public var xAxisGridStyle : GridStyle + public var yAxisGridStyle : GridStyle + public var xAxisLabelPosition : XAxisLabelPosistion + public var xAxisLabelsFrom : LabelsFrom + public var yAxisLabelPosition : YAxisLabelPosistion + public var yAxisNumberOfLabels : Int + public var baseline : Baseline /// Model for controlling the overall aesthetic of the chart. /// - Parameters: diff --git a/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift index dee71eeb..6d793738 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift @@ -7,14 +7,99 @@ import SwiftUI +/** + Data set for a single line + + Contains information specific to each line such as: + + # Example + ``` + let data = LineDataSet(dataPoints: [ + LineChartDataPoint(value: 20, xAxisLabel: "M", pointLabel: "Monday"), + LineChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"), + LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), + LineChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"), + LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), + LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), + LineChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") + ], + legendTitle: "Data", + pointStyle: PointStyle(), + style: LineStyle()) + ``` + + # LineChartDataPoint + ``` + LineChartDataPoint(value: Double, + xAxisLabel: String?, + pointLabel: String?, + date: Date?) + ``` + + # PointStyle + ``` + PointStyle(pointSize: CGFloat, + borderColour: Color, + fillColour: Color, + lineWidth: CGFloat, + pointType: PointType, + pointShape: PointShape) + ``` + + # LineStyle + ``` + LineStyle(colour: Color, + ...) + + LineStyle(colours: [Color], + startPoint: UnitPoint, + endPoint: UnitPoint, + ...) + + LineStyle(stops: [GradientStop], + startPoint: UnitPoint, + endPoint: UnitPoint, + ...) + + LineStyle(..., + lineType: LineType, + strokeStyle: Stroke, + ignoreZero: Bool) + ``` + --- + # Also See + - [LineChartDataPoint](x-source-tag://LineChartDataPoint) + - [PointStyle](x-source-tag://PointStyle) + - [PointType](x-source-tag://PointType) + - [PointShape](x-source-tag://PointShape) + - [LineStyle](x-source-tag://LineStyle) + - [ColourType](x-source-tag://ColourType) + - [LineType](x-source-tag://LineType) + - [GradientStop](x-source-tag://GradientStop) + + # Conforms to + - SingleDataSet + - DataSet + - Hashable + - Identifiable + + - Tag: LineDataSet + */ public struct LineDataSet: SingleDataSet { public let id : UUID public var dataPoints : [LineChartDataPoint] public var legendTitle : String public var pointStyle : PointStyle - public var style : Styling + public var style : LineStyle + + /// Initialises a new data set for 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 drawin. public init(dataPoints : [LineChartDataPoint], legendTitle : String, pointStyle : PointStyle = PointStyle(), @@ -31,7 +116,10 @@ public struct LineDataSet: SingleDataSet { public typealias Styling = LineStyle } - +/** + + - Tag: MultiLineDataSet + */ public struct MultiLineDataSet: MultiDataSet { public let id : UUID diff --git a/Sources/SwiftUICharts/LineChart/Models/LineStyle.swift b/Sources/SwiftUICharts/LineChart/Models/LineStyle.swift index 47cfe925..3fbfb396 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineStyle.swift @@ -7,27 +7,76 @@ import SwiftUI -/// Model for controlling the aesthetic of the line chart. +/** + Model for controlling the aesthetic of the line chart. + + # Example + + ``` + LineStyle(colour: .red, + lineType: .curvedLine, + strokeStyle: Stroke(lineWidth: 2, + lineCap: .round, + lineJoin: .round, + miterLimit: 10, + dash: [CGFloat](), + dashPhase: 0), + ignoreZero: false) + ``` + + --- + + # Options + + ``` + LineStyle(colour: Color, + ...) + + LineStyle(colours: [Color], + startPoint: UnitPoint, + endPoint: UnitPoint, + ...) + + LineStyle(stops: [GradientStop], + startPoint: UnitPoint, + endPoint: UnitPoint, + ...) + + LineStyle(..., + lineType: LineType, + strokeStyle: Stroke, + ignoreZero: Bool) + ``` + + # Also See + - [ColourType](x-source-tag://ColourType) + - [LineType](x-source-tag://LineType) + - [GradientStop](x-source-tag://GradientStop) + + # Conforms to + - CTColourStyle + - Hashable + + - Tag: LineStyle + */ public struct LineStyle: CTColourStyle, Hashable { - /// Type of colour styling for the chart. - public var colourType : ColourType - /// Drawing style of the line - public var lineType : LineType - - public var strokeStyle : Stroke - - /// Single Colour + public var colourType : ColourType 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? + + /// Drawing style of the line + public var lineType : LineType + + /** + Styling for stroke + + Replica of Appleā€™s StrokeStyle that conforms to Hashable + */ + public var strokeStyle : Stroke /** Whether the chart should skip data points who's value is 0. diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift index 9f076ca8..e95e4241 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -7,7 +7,161 @@ import SwiftUI -/// The central model from which the chart is drawn. +/** + 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. + + # Example + ``` + static func weekOfData() -> MultiLineChartData { + + let data = MultiLineDataSet(dataSets: [ + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 20, xAxisLabel: "M", pointLabel: "Monday"), + LineChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"), + LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), + LineChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"), + LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), + LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), + LineChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") + ], + legendTitle: "Test One", + pointStyle: PointStyle(), + style: LineStyle(colour: Color.red)), + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 90, xAxisLabel: "M", pointLabel: "Monday"), + LineChartDataPoint(value: 20, xAxisLabel: "T", pointLabel: "Tuesday"), + LineChartDataPoint(value: 120, xAxisLabel: "W", pointLabel: "Wednesday"), + LineChartDataPoint(value: 85, xAxisLabel: "T", pointLabel: "Thursday"), + LineChartDataPoint(value: 140, xAxisLabel: "F", pointLabel: "Friday"), + LineChartDataPoint(value: 10, xAxisLabel: "S", pointLabel: "Saturday"), + LineChartDataPoint(value: 20, xAxisLabel: "S", pointLabel: "Sunday") + ], + legendTitle: "Test Two", + pointStyle: PointStyle(), + style: LineStyle(colour: Color.blue))]) + + let metadata = ChartMetadata(title: "Some Data", subtitle: "A Week") + let labels = ["Monday", "Thursday", "Sunday"] + + return MultiLineChartData(dataSets: data, + metadata: metadata, + xAxisLabels: labels, + chartStyle: LineChartStyle(baseline: .zero), + calculations: .none) + } +} + + ``` + + --- + + # Parts + + ## LineDataSet + ``` + LineDataSet(dataPoints: [LineChartDataPoint], + legendTitle: String, + pointStyle: PointStyle, + style: LineStyle) + ``` + ### LineChartDataPoint + ``` + LineChartDataPoint(value: Double, + xAxisLabel: String?, + pointLabel: String?, + date: Date?) + ``` + + ### PointStyle + ``` + PointStyle(pointSize: CGFloat, + borderColour: Color, + fillColour: Color, + lineWidth: CGFloat, + pointType: PointType, + pointShape: PointShape) + ``` + + ### LineStyle + ``` + LineStyle(colour: Color, + ...) + + LineStyle(colours: [Color], + startPoint: UnitPoint, + endPoint: UnitPoint, + ...) + + LineStyle(stops: [GradientStop], + startPoint: UnitPoint, + endPoint: UnitPoint, + ...) + + LineStyle(..., + lineType: LineType, + strokeStyle: Stroke, + ignoreZero: Bool) + ``` + + ## ChartMetadata + ``` + ChartMetadata(title: String?, subtitle: String?) + ``` + + ## LineChartStyle + + ``` + LineChartStyle(infoBoxPlacement : InfoBoxPlacement, + xAxisGridStyle : GridStyle, + yAxisGridStyle : GridStyle, + xAxisLabelPosition : XAxisLabelPosistion, + xAxisLabelsFrom : LabelsFrom, + yAxisLabelPosition : YAxisLabelPosistion, + yAxisNumberOfLabels : Int, + baseline : Baseline, + globalAnimation : Animation) + ``` + + ### GridStyle + ``` + GridStyle(numberOfLines: Int, + lineColour : Color, + lineWidth : CGFloat, + dash : [CGFloat], + dashPhase : CGFloat) + ``` + + --- + + # Also See + - [Line Data Set](x-source-tag://LineDataSet) + - [Line Chart Data Point](x-source-tag://LineChartDataPoint) + - [Point Style](x-source-tag://PointStyle) + - [PointType](x-source-tag://PointType) + - [PointShape](x-source-tag://PointShape) + - [Line Style](x-source-tag://LineStyle) + - [ColourType](x-source-tag://ColourType) + - [LineType](x-source-tag://LineType) + - [GradientStop](x-source-tag://GradientStop) + - [Chart Metadata](x-source-tag://ChartMetadata) + - [Line Chart Style](x-source-tag://LineChartStyle) + - [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) + - [GridStyle](x-source-tag://GridStyle) + - [XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) + - [LabelsFrom](x-source-tag://LabelsFrom) + - [YAxisLabelPosistion](x-source-tag://YAxisLabelPosistion) + + # Conforms to + - ObservableObject + - Identifiable + - LineChartDataProtocol + - LineAndBarChartData + - ChartData + + - Tag: LineChartData + */ public class MultiLineChartData: LineChartDataProtocol { public let id : UUID = UUID() diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index d22e6661..1599f72d 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -7,6 +7,91 @@ import SwiftUI +/** + View for drawing a line graph. + + This creates a single line, line chart. + + # Example + ## Data Initialisation + ``` + let data : LineChartData = makeData() + ``` + ## 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. + ``` + .touchOverlay(chartData: data) + .pointMarkers(chartData: data) + .averageLine(chartData: data) + .yAxisPOI(chartData: data) + .xAxisGrid(chartData: data) + .yAxisGrid(chartData: data) + .xAxisLabels(chartData: data) + .yAxisLabels(chartData: data) + .headerBox(chartData: data) + .legends(chartData: data) + ``` + - [Touch Overlay](x-source-tag://TouchOverlay) + - [Point Markers](x-source-tag://PointMarkers) + - [Average Line](x-source-tag://AverageLine) + - [Y Axis POI](x-source-tag://YAxisPOI) + - [X Axis Grid](x-source-tag://XAxisGrid) + - [Y Axis Grid](x-source-tag://YAxisGrid) + - [X Axis Labels](x-source-tag://XAxisLabels) + - [Y Axis Labels](x-source-tag://YAxisLabels) + - [Header Box](x-source-tag://HeaderBox) + - [Legends](x-source-tag://Legends) + + ## Data Model + `LineChartData` is the central model + ``` + static func makeData() -> LineChartData { + + let data = LineDataSet(dataPoints: [ + LineChartDataPoint(value: 20, xAxisLabel: "M", pointLabel: "Monday"), + LineChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"), + LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), + LineChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"), + LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), + LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), + LineChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") + ], + legendTitle: "Data", + pointStyle: PointStyle(), + style: LineStyle() + + let metadata = ChartMetadata(title: "Some Data", subtitle: "A Week") + + let labels = ["Monday", "Thursday", "Sunday"] + + return LineChartData(dataSets: data, + metadata: metadata, + xAxisLabels: labels, + chartStyle: LineChartStyle(), + calculations: .none) + } + + ``` + + --- + + # Also See + - [LineDataSet](x-source-tag://LineDataSet) + - [ChartMetadata](x-source-tag://ChartMetadata) + - [LineChartStyle](x-source-tag://LineChartStyle) + + # Conforms to + - View + + - Tag: ChartData + */ public struct LineChart: View where ChartData: LineChartData { @ObservedObject var chartData: ChartData diff --git a/Sources/SwiftUICharts/Shared/Extras/Enums.swift b/Sources/SwiftUICharts/Shared/Extras/Enums.swift index e76daf5b..6a970be5 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Enums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Enums.swift @@ -160,9 +160,9 @@ public enum InfoBoxPlacement { case topTrailing // From top and trailing edges meeting at touch location ``` - - Tag: MarkerLineType + - Tag: MarkerType */ -public enum MarkerLineType { +public enum MarkerType { /// Full width and height of view intersecting at a specified point case fullWidth /// From bottom and leading edges meeting at a specified point diff --git a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift index ed801e4b..191590c8 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift @@ -7,18 +7,27 @@ import Foundation -/// Data model for the chart's metadata -/// -/// - Tag: ChartMetadata +/** + Data model for the chart's metadata + + Contains the Title, Subtitle and Title for Legend. + + # Example + ``` + let metadata = ChartMetadata(title: "Some Data", subtitle: "A Week") + ``` + + - Tag: ChartMetadata + */ public struct ChartMetadata { - /// The charts Title + /// The charts title var title : String? /// The charts subtitle var subtitle : String? /// Model to hold the metadata for the chart. /// - Parameters: - /// - title: The charts Title + /// - title: The charts title /// - subtitle: The charts subtitle public init(title : String? = nil, subtitle : String? = nil diff --git a/Sources/SwiftUICharts/Shared/Models/GridStyle.swift b/Sources/SwiftUICharts/Shared/Models/GridStyle.swift index d4fffe03..91925764 100644 --- a/Sources/SwiftUICharts/Shared/Models/GridStyle.swift +++ b/Sources/SwiftUICharts/Shared/Models/GridStyle.swift @@ -7,8 +7,29 @@ import SwiftUI -/// Model for controlling the look of the Grid -/// - Tag: GridStyle +/** + Controlling for the look of the Grid + + # Example + ``` + GridStyle(numberOfLines: 7, + lineColour : .gray, + lineWidth : 1, + dash : [8], + dashPhase : 0) + ``` + + # Options + ``` + GridStyle(numberOfLines: Int, + lineColour : Color, + lineWidth : CGFloat, + dash : [CGFloat], + dashPhase : CGFloat) + ``` + + - Tag: GridStyle + */ public struct GridStyle { /// Number of lines to break up the axis diff --git a/Sources/SwiftUICharts/Shared/Models/HashablePoint.swift b/Sources/SwiftUICharts/Shared/Models/HashablePoint.swift new file mode 100644 index 00000000..bfd8461b --- /dev/null +++ b/Sources/SwiftUICharts/Shared/Models/HashablePoint.swift @@ -0,0 +1,22 @@ +// +// HashablePoint.swift +// +// +// Created by Will Dale on 03/02/2021. +// + +import SwiftUI + +/** + A hashable version of CGPoint + */ +public struct HashablePoint: Hashable { + + public let x : CGFloat + public let y : CGFloat + + public init(x: CGFloat, y: CGFloat) { + self.x = x + self.y = y + } +} diff --git a/Sources/SwiftUICharts/Shared/Models/PointStyle.swift b/Sources/SwiftUICharts/Shared/Models/PointStyle.swift index f9f4cd26..9bc42c7a 100644 --- a/Sources/SwiftUICharts/Shared/Models/PointStyle.swift +++ b/Sources/SwiftUICharts/Shared/Models/PointStyle.swift @@ -7,23 +7,59 @@ import SwiftUI -/// Model for controlling the aesthetic of the point markers. +/** + 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: Color.primary, + fillColour: Color.red, + lineWidth: 2, pointType: .filledOutLine, + pointShape: .circle) + ``` + + # Options + ``` + PointStyle(pointSize: CGFloat, + borderColour: Color, + fillColour: Color, + lineWidth: CGFloat, + pointType: PointType, + pointShape: PointShape) + ``` + # Also See + - [PointType](x-source-tag://PointType) + - [PointShape](x-source-tag://PointShape) + + # Conforms to + - Hashable + + - Tag: PointStyle + */ 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/Shared/Models/Protocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols.swift index 9184e418..9a4fe7f7 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols.swift @@ -7,6 +7,8 @@ import SwiftUI + +// MARK: Chart Data /** Main protocol for passing data around library. @@ -28,10 +30,10 @@ public protocol ChartData: ObservableObject, Identifiable { var dataSets: Set { get set } /** - Data model containing: the charts Title, the charts Subtitle and the Line Legend. + Data model containing the charts Title, Subtitle and the Title for Legend. # Reference - [ChartType](x-source-tag://ChartType) + [ChartMetadata](x-source-tag://ChartMetadata) */ var metadata: ChartMetadata? { get set } @@ -53,7 +55,7 @@ public protocol ChartData: ObservableObject, Identifiable { var noDataText: Text { get set } /** - Holds metadata about the chart. + Holds data about the charts type. Allows for internal logic based on the type of chart. @@ -145,7 +147,9 @@ public protocol SingleDataSet: DataSet { associatedtype DataPoint : CTChartDataPoint /** - Array of data points. [See CTChartDataPoint](x-source-tag://CTChartDataPoint) + Array of data points. + + [See CTChartDataPoint](x-source-tag://CTChartDataPoint) */ var dataPoints : [DataPoint] { get set } @@ -261,9 +265,12 @@ public protocol CTColourStyle { */ public protocol CTChartDataPoint: Hashable, Identifiable { + var id : ID { get } - /// Value of the data point + /** + Value of the data point + */ var value : Double { get set } /** diff --git a/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift b/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift index 31c5cc69..64b7aaee 100644 --- a/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift +++ b/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift @@ -11,11 +11,11 @@ import SwiftUI internal struct TouchOverlayMarker: Shape { /// Where the marker lines come from to meet at a specified point - private var type : MarkerLineType = .fullWidth + private var type : MarkerType = .fullWidth /// Point that the marker lines should intersect private var position : CGPoint - internal init(type : MarkerLineType = .fullWidth, + internal init(type : MarkerType = .fullWidth, position : HashablePoint ) { self.type = type diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index e83872d7..a0551499 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -102,8 +102,19 @@ internal struct HeaderBox: ViewModifier where T: ChartData { } extension View { - /// Displays the metadata about the chart - /// - Returns: Chart title and subtitle. + /** + Displays the metadata about the chart + + Adds a view above the chart that displays the title and subtitle. + 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. + + - Tag: HeaderBox + */ public func headerBox(chartData: T) -> some View { self.modifier(HeaderBox(chartData: chartData)) } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift index 80f00360..00ca3d8f 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift @@ -20,8 +20,14 @@ internal struct Legends: ViewModifier where T: ChartData { } extension View { - /// Displays legends under the chart. - /// - Returns: Legends from the charts data and any markers. + /** + Displays legends under the chart. + + - Parameter chartData: Chart data model. + - Returns: A new view containing the chart with chart legends under. + + - Tag: Legends + */ public func legends(chartData: T) -> some View { self.modifier(Legends(chartData: chartData)) } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift index 5601845a..90eefe60 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift @@ -8,7 +8,7 @@ import SwiftUI -/// - Tag: PointMarkers + internal struct PointMarkers: ViewModifier where T: LineChartDataProtocol { @ObservedObject var chartData: T @@ -50,25 +50,60 @@ internal struct PointMarkers: ViewModifier where T: LineChartDataProtocol { } 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 + /** + 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 + + - Requires: + Chart Data to conform to LineChartDataProtocol. + - LineChartData + - MultiLineChartData + + # Available for: + - Line Chart + - Multi Line Chart + + # Unavailable for: + - Bar Chart + - Grouped Bar Chart + - Pie Chart + - Doughnut Chart + + - Parameter chartData: Chart data model. + - Returns: A new view containing the chart with point markers. + + - Tag: PointMarkers + */ public func pointMarkers(chartData: T) -> some View { self.modifier(PointMarkers(chartData: chartData)) } } -struct PointsSubView: View { +internal struct PointsSubView: View { - let dataSets: LineDataSet - let minValue : Double - let range : Double - let animation: Animation - let isFilled : Bool + private let dataSets: LineDataSet + private let minValue : Double + private let range : Double + private let animation: Animation + private let isFilled : Bool @State var startAnimation : Bool = false - var body: some View { + internal init(dataSets : LineDataSet, + minValue : Double, + range : Double, + animation : Animation, + isFilled : Bool + ) { + self.dataSets = dataSets + self.minValue = minValue + self.range = range + self.animation = animation + self.isFilled = isFilled + } + + internal var body: some View { switch dataSets.pointStyle.pointType { case .filled: diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 9b87fce7..9a18958b 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -8,7 +8,12 @@ import SwiftUI #if !os(tvOS) -/// Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information. +/** + Detects input either from touch of pointer. + + Finds the nearest data point and displays the relevent information. + + */ internal struct TouchOverlay: ViewModifier where T: ChartData { @ObservedObject var chartData: T @@ -130,12 +135,37 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { extension View { #if !os(tvOS) - /// 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(chartData: T, specifier: String = "%.0f") -> 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 LineChartStyle --> infoBoxPlacement is set to .header + then `.headerBox` is required. + + - Attention: + Unavailable in tvOS + + - Parameters: + - chartData: Chart data model. + - specifier: Decimal precision for labels. + - Returns: A new view containing the chart with a touch overlay. + + - Tag: TouchOverlay + */ + public func touchOverlay(chartData: T, + specifier: String = "%.0f" + ) -> some View { self.modifier(TouchOverlay(chartData: chartData, specifier: specifier)) } #elseif os(tvOS) + /** + Adds touch interaction with the chart. + + - Attention: + Unavailable in tvOS + */ public func touchOverlay(specifier: String = "%.0f") -> some View { self.modifier(EmptyModifier()) } @@ -143,13 +173,4 @@ extension View { } -public struct HashablePoint: Hashable { - public let x : CGFloat - public let y : CGFloat - - public init(x: CGFloat, y: CGFloat) { - self.x = x - self.y = y - } -} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift index 777e8c02..7aab03bb 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift @@ -33,6 +33,26 @@ internal struct XAxisGrid: ViewModifier where T: LineAndBarChartData { extension View { /** Adds vertical lines along the X axis. + + The style is set in ChartData --> LineChartStyle --> xAxisGridStyle + + - Requires: + Chart Data to conform to LineAndBarChartData. + + # Available for: + - Line Chart + - Multi Line Chart + - Bar Chart + - Grouped 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. + + - Tag: XAxisGrid */ public func xAxisGrid(chartData: T) -> some View { self.modifier(XAxisGrid(chartData: chartData)) diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift index f4095cb7..1a6036d1 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift @@ -86,7 +86,30 @@ internal struct XAxisLabels: ViewModifier where T: LineAndBarChartData { } extension View { - /// Labels for the X axis. + /** + Labels for the X axis. + + The labels can either come from ChartData --> xAxisLabels + or ChartData --> DataSets --> DataPoints + + - Requires: + Chart Data to conform to LineAndBarChartData. + + # Available for: + - Line Chart + - Multi Line Chart + - Bar Chart + - Grouped 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. + + - Tag: XAxisLabels + */ public func xAxisLabels(chartData: T) -> some View { self.modifier(XAxisLabels(chartData: chartData)) } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift index 86aaef67..801249ba 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift @@ -34,9 +34,27 @@ internal struct YAxisGrid: ViewModifier where T: LineAndBarChartData { extension View { /** - Adds horizontal lines along the Y axis. - - Parameter numberOfLines: Number of lines subdividing the chart - - Returns: View of evenly spaced horizontal lines + Adds horizontal lines along the X axis. + + The style is set in ChartData --> LineChartStyle --> yAxisGridStyle + + - Requires: + Chart Data to conform to LineAndBarChartData. + + # Available for: + - Line Chart + - Multi Line Chart + - Bar Chart + - Grouped 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. + + - Tag: YAxisGrid */ public func yAxisGrid(chartData: T) -> some View { self.modifier(YAxisGrid(chartData: chartData)) diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift index 9ffae1b5..fcadda25 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift @@ -80,9 +80,26 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { extension View { /** Automatically generated labels for the Y axis + + Controls are in ChartData --> ChartStyle + + - Requires: + Chart Data to conform to LineAndBarChartData. + + # Available for: + - Line Chart + - Multi Line Chart + - Bar Chart + - Grouped Bar Chart + + # Unavailable for: + - Pie Chart + - Doughnut Chart + - Parameters: - specifier: Decimal precision specifier - Returns: HStack of labels + - Tag: YAxisLabels */ public func yAxisLabels(chartData: T, specifier: String = "%.0f") -> some View { self.modifier(YAxisLabels(chartData: chartData, specifier: specifier)) diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift index af3139de..910af3eb 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift @@ -90,23 +90,52 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { } extension View { - /// Shows a marker line at chosen point. - /// - Parameters: - /// - markerName: Title of marker, for the legend - /// - markerValue : Chosen point. - /// - lineColour: Line Colour - /// - strokeStyle: Style of Stroke - /// - Returns: A marker line at the average of all the data points. + /** + Horizontal line marking a custom value + + Shows a marker line at a specified value. + + # Example + ``` + .yAxisPOI(chartData: data, + markerName: "Marker", + lineColour: .blue, + strokeStyle: StrokeStyle(lineWidth: 2, + lineCap: .round, + lineJoin: .round, + miterLimit: 10, + dash: [8], + dashPhase: 0)) + ``` + + - Requires: + Chart Data to conform to LineAndBarChartData. + + # Available for: + - Line Chart + - Multi Line Chart + - Bar Chart + - Grouped Bar Chart + + # Unavailable for: + - Pie Chart + - Doughnut Chart + + - Parameters: + - chartData: Chart data model. + - markerName: Title of marker, for the legend. + - markerValue: Value to mark + - lineColour: Line Colour. + - strokeStyle: Style of Stroke. + - Returns: A new view containing the chart with a marker line at a specified value. + + - Tag: YAxisPOI + */ public func yAxisPOI(chartData : T, markerName : String, markerValue : Double, lineColour : Color = Color(.blue), - strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 2, - lineCap: .round, - lineJoin: .round, - miterLimit: 10, - dash: [CGFloat](), - dashPhase: 0) + strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round, miterLimit: 10, dash: [CGFloat](), dashPhase: 0) ) -> some View { self.modifier(YAxisPOI(chartData : chartData, markerName : markerName, @@ -117,21 +146,51 @@ extension View { } - /// Shows a marker line at the average of all the data points. - /// - Parameters: - /// - markerName: Title of marker, for the legend - /// - lineColour: Line Colour - /// - strokeStyle: Style of Stroke - /// - Returns: A marker line at the average of all the data points. + /** + 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", + lineColour: .primary, + strokeStyle: StrokeStyle(lineWidth: 2, + lineCap: .round, + lineJoin: .round, + miterLimit: 10, + dash: [8], + dashPhase: 0)) + ``` + + - Requires: + Chart Data to conform to LineAndBarChartData. + + # Available for: + - Line Chart + - Multi Line Chart + - Bar Chart + - Grouped Bar Chart + + # Unavailable for: + - Pie Chart + - Doughnut Chart + + - Parameters: + - chartData: Chart data model. + - markerName: Title of marker, for the legend. + - 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", lineColour : Color = Color.primary, - strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 2, - lineCap: .round, - lineJoin: .round, - miterLimit: 10, - dash: [CGFloat](), - dashPhase: 0) + strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round, miterLimit: 10, dash: [CGFloat](), dashPhase: 0) ) -> some View { self.modifier(YAxisPOI(chartData : chartData, markerName : markerName, From af878859dd22e5dc8c5fd50be48b7f45dcfdee6b Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 4 Feb 2021 14:13:20 +0000 Subject: [PATCH 020/152] Add new case to Baseline Enum. Refactor Baseline switch into protocol. --- .../LineChart/Models/LineChartData.swift | 14 +---- .../LineChart/Models/LineChartProtocols.swift | 62 ++++++++++++++----- .../LineChart/Models/MultiLineChartData.swift | 13 +--- .../LineChart/Views/FilledLineChart.swift | 10 +-- .../LineChart/Views/LineChartView.swift | 11 +--- .../LineChart/Views/MultiLineChart.swift | 11 +--- .../SwiftUICharts/Shared/Extras/Enums.swift | 2 + .../Shared/ViewModifiers/YAxisPOI.swift | 33 ++-------- 8 files changed, 64 insertions(+), 92 deletions(-) diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index 9c19a364..abcdedf4 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -257,17 +257,8 @@ public class LineChartData: LineChartDataProtocol { public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { var locations : [HashablePoint] = [] - let minValue : Double - let range : Double - - switch self.chartStyle.baseline { - case .minimumValue: - minValue = self.getMinValue() - range = self.getRange() - case .zero: - minValue = 0 - range = self.getMaxValue() - } + let minValue : Double = self.getMinValue() + let range : Double = self.getRange() let ySection : CGFloat = chartSize.size.height / CGFloat(range) let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count - 1) @@ -317,7 +308,6 @@ public class LineChartData: LineChartDataProtocol { chartType : .line)) } } - public typealias Set = LineDataSet public typealias DataPoint = LineChartDataPoint } diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift index 94e52a3a..4cc4cde9 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift @@ -25,20 +25,10 @@ public protocol LineChartDataProtocol: LineAndBarChartData where CTStyle: CTLine extension LineAndBarChartData where Self: LineChartDataProtocol { public func getYLabels() -> [Double] { var labels : [Double] = [Double]() - let dataRange : Double - let minValue : Double - let range : Double - - switch self.chartStyle.baseline { - case .minimumValue: - minValue = self.getMinValue() - dataRange = self.getRange() - range = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) - case .zero: - minValue = 0 - dataRange = self.getMaxValue() - range = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) - } + let dataRange : Double = self.getRange() + let minValue : Double = self.getMinValue() + let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) + labels.append(minValue) for index in 1...self.chartStyle.yAxisNumberOfLabels { labels.append(minValue + range * Double(index)) @@ -46,6 +36,50 @@ extension LineAndBarChartData where Self: LineChartDataProtocol { return labels } } +extension LineAndBarChartData where Self: LineChartData { + public func getRange() -> Double { + switch self.chartStyle.baseline { + case .minimumValue: + return DataFunctions.dataSetRange(from: dataSets) + case .minimumWithMaximum(of: let value): + return DataFunctions.dataSetMaxValue(from: dataSets) - min(DataFunctions.dataSetMinValue(from: dataSets), value) + case .zero: + return DataFunctions.dataSetMaxValue(from: dataSets) + } + } + public func getMinValue() -> Double { + switch self.chartStyle.baseline { + case .minimumValue: + return DataFunctions.dataSetMinValue(from: dataSets) + case .minimumWithMaximum(of: let value): + return min(DataFunctions.dataSetMinValue(from: dataSets), value) + case .zero: + return 0 + } + } +} +extension LineAndBarChartData where Self: MultiLineChartData { + public func getRange() -> Double { + switch self.chartStyle.baseline { + case .minimumValue: + return DataFunctions.multiDataSetRange(from: dataSets) + case .minimumWithMaximum(of: let value): + return DataFunctions.multiDataSetMaxValue(from: dataSets) - min(DataFunctions.multiDataSetMinValue(from: dataSets), value) + case .zero: + return DataFunctions.multiDataSetMaxValue(from: dataSets) + } + } + public func getMinValue() -> Double { + switch self.chartStyle.baseline { + case .minimumValue: + return DataFunctions.multiDataSetMinValue(from: dataSets) + case .minimumWithMaximum(of: let value): + return min(DataFunctions.multiDataSetMinValue(from: dataSets), value) + case .zero: + return 0 + } + } +} /** A protocol to extend functionality of `CTLineAndBarChartStyle` specifically for Line Charts. diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift index e95e4241..4553195f 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -240,17 +240,8 @@ public class MultiLineChartData: LineChartDataProtocol { var locations : [HashablePoint] = [] for dataSet in dataSets.dataSets { - let minValue : Double - let range : Double - - switch self.chartStyle.baseline { - case .minimumValue: - minValue = self.getMinValue() - range = self.getRange() - case .zero: - minValue = 0 - range = self.getMaxValue() - } + let minValue : Double = self.getMinValue() + let range : Double = self.getRange() let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) let ySection : CGFloat = chartSize.size.height / CGFloat(range) diff --git a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift index e5722ac0..fb263ff6 100644 --- a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift @@ -18,15 +18,9 @@ public struct FilledLineChart: View where ChartData: LineChartData { public init(chartData: ChartData) { self.chartData = chartData + self.minValue = chartData.getMinValue() + self.range = chartData.getRange() - switch chartData.chartStyle.baseline { - case .minimumValue: - self.minValue = chartData.getMinValue() - self.range = chartData.getRange() - case .zero: - self.minValue = 0 - self.range = chartData.getMaxValue() - } self.chartData.isFilled = true } diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index 1599f72d..9c6fbd6b 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -101,15 +101,8 @@ public struct LineChart: View where ChartData: LineChartData { public init(chartData: ChartData) { self.chartData = chartData - - switch chartData.chartStyle.baseline { - case .minimumValue: - self.minValue = chartData.getMinValue() - self.range = chartData.getRange() - case .zero: - self.minValue = 0 - self.range = chartData.getMaxValue() - } + self.minValue = chartData.getMinValue() + self.range = chartData.getRange() } public var body: some View { diff --git a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift index 7cd9dfa3..b0ce5a23 100644 --- a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift @@ -18,15 +18,8 @@ public struct MultiLineChart: View where ChartData: MultiLineChartDat public init(chartData: ChartData) { self.chartData = chartData - - switch chartData.chartStyle.baseline { - case .minimumValue: - self.minValue = chartData.getMinValue() - self.range = chartData.getRange() - case .zero: - self.minValue = 0 - self.range = chartData.getMaxValue() - } + self.minValue = chartData.getMinValue() + self.range = chartData.getRange() } public var body: some View { diff --git a/Sources/SwiftUICharts/Shared/Extras/Enums.swift b/Sources/SwiftUICharts/Shared/Extras/Enums.swift index 6a970be5..9ea5c94f 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Enums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Enums.swift @@ -114,6 +114,8 @@ public enum LineType { 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 } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift index 910af3eb..9aebe54e 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift @@ -33,35 +33,10 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { self.lineColour = lineColour self.strokeStyle = strokeStyle - if chartData.chartType.chartType == .line { - - let chartData = chartData as! LineChartData - - switch chartData.chartStyle.baseline { - case .minimumValue: - - self.markerValue = isAverage ? chartData.getAverage() : markerValue - self.range = chartData.getRange() - self.minValue = chartData.getMinValue() - self.maxValue = chartData.getMaxValue() - - case .zero: - - self.markerValue = isAverage ? chartData.getAverage() : markerValue - self.range = chartData.getRange() - self.minValue = 0 - self.maxValue = chartData.getMaxValue() - - } - - } else { - self.markerValue = isAverage ? chartData.getAverage() : markerValue - self.range = chartData.getRange() - self.minValue = chartData.getMinValue() - self.maxValue = chartData.getMaxValue() - } - - + self.markerValue = isAverage ? chartData.getAverage() : markerValue + self.maxValue = chartData.getMaxValue() + self.range = chartData.getRange() + self.minValue = chartData.getMinValue() } internal func body(content: Content) -> some View { From 24c8265fbb94405c6e6b5b8d944db00b695a6c60 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 4 Feb 2021 14:15:42 +0000 Subject: [PATCH 021/152] Tidy up. --- .../LineChart/Models/LineDataSet.swift | 19 +-- .../LineChart/Models/MultiLineDataSet.swift | 126 ++++++++++++++++++ .../ViewModifiers/PointMarkers.swift | 69 ++++++++++ .../Views/SubViews/PointsSubView.swift} | 80 +---------- .../Shared/Models/ChartViewData.swift | 34 ----- .../Shared/Models/InfoViewData.swift | 42 ++++++ .../LineAndBar/LineAndBarProtocols.swift | 1 + ...{Protocols.swift => SharedProtocols.swift} | 2 +- .../{Models => Types}/GradientStop.swift | 0 .../{Models => Types}/HashablePoint.swift | 0 .../Shared/{Models => Types}/Stroke.swift | 0 11 files changed, 244 insertions(+), 129 deletions(-) create mode 100644 Sources/SwiftUICharts/LineChart/Models/MultiLineDataSet.swift create mode 100644 Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift rename Sources/SwiftUICharts/{Shared/ViewModifiers/PointMarkers.swift => LineChart/Views/SubViews/PointsSubView.swift} (60%) create mode 100644 Sources/SwiftUICharts/Shared/Models/InfoViewData.swift rename Sources/SwiftUICharts/Shared/Models/{Protocols.swift => SharedProtocols.swift} (99%) rename Sources/SwiftUICharts/Shared/{Models => Types}/GradientStop.swift (100%) rename Sources/SwiftUICharts/Shared/{Models => Types}/HashablePoint.swift (100%) rename Sources/SwiftUICharts/Shared/{Models => Types}/Stroke.swift (100%) diff --git a/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift index 6d793738..0423283c 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift @@ -10,7 +10,7 @@ import SwiftUI /** Data set for a single line - Contains information specific to each line such as: + Contains information specific to each line within the chart . # Example ``` @@ -115,20 +115,3 @@ public struct LineDataSet: SingleDataSet { public typealias ID = UUID public typealias Styling = LineStyle } - -/** - - - Tag: MultiLineDataSet - */ -public struct MultiLineDataSet: MultiDataSet { - - public let id : UUID - - public var dataSets : [LineDataSet] - - public init(dataSets: [LineDataSet]) { - self.id = UUID() - self.dataSets = dataSets - } - -} diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineDataSet.swift new file mode 100644 index 00000000..d7a6d643 --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineDataSet.swift @@ -0,0 +1,126 @@ +// +// MultiLineDataSet.swift +// +// +// Created by Will Dale on 04/02/2021. +// + +import SwiftUI + +/** + Data set for a multiple lines + + Contains information about each of lines within the chart. + + + ``` + let data = MultiLineDataSet(dataSets: [ + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 60, xAxisLabel: "M", pointLabel: "Monday"), + LineChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"), + LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), + LineChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"), + LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), + LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), + LineChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") + ], + legendTitle: "Test One", + pointStyle: PointStyle(), + style: LineStyle(colour: Color.red)), + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 90, xAxisLabel: "M", pointLabel: "Monday"), + LineChartDataPoint(value: 60, xAxisLabel: "T", pointLabel: "Tuesday"), + LineChartDataPoint(value: 120, xAxisLabel: "W", pointLabel: "Wednesday"), + LineChartDataPoint(value: 85, xAxisLabel: "T", pointLabel: "Thursday"), + LineChartDataPoint(value: 140, xAxisLabel: "F", pointLabel: "Friday"), + LineChartDataPoint(value: 80, xAxisLabel: "S", pointLabel: "Saturday"), + LineChartDataPoint(value: 50, xAxisLabel: "S", pointLabel: "Sunday") + ], + legendTitle: "Test Two", + pointStyle: PointStyle(), + style: LineStyle(colour: Color.blue))]) + ``` + + # DataSet + ``` + LineDataSet(dataPoints: [LineChartDataPoint], + legendTitle: String, + pointStyle: PointStyle, + style: LineStyle) + ``` + + + # LineChartDataPoint + ``` + LineChartDataPoint(value: Double, + xAxisLabel: String?, + pointLabel: String?, + date: Date?) + ``` + + + # PointStyle + ``` + PointStyle(pointSize: CGFloat, + borderColour: Color, + fillColour: Color, + lineWidth: CGFloat, + pointType: PointType, + pointShape: PointShape) + ``` + + # LineStyle + ``` + LineStyle(colour: Color, + ...) + + LineStyle(colours: [Color], + startPoint: UnitPoint, + endPoint: UnitPoint, + ...) + + LineStyle(stops: [GradientStop], + startPoint: UnitPoint, + endPoint: UnitPoint, + ...) + + LineStyle(..., + lineType: LineType, + strokeStyle: Stroke, + ignoreZero: Bool) + ``` + + --- + # Also See + - [LineDataSet](x-source-tag://LineDataSet) + - [LineChartDataPoint](x-source-tag://LineChartDataPoint) + - [PointStyle](x-source-tag://PointStyle) + - [PointType](x-source-tag://PointType) + - [PointShape](x-source-tag://PointShape) + - [LineStyle](x-source-tag://LineStyle) + - [ColourType](x-source-tag://ColourType) + - [LineType](x-source-tag://LineType) + - [GradientStop](x-source-tag://GradientStop) + + # Conforms to + - MultiDataSet + - DataSet + - Hashable + - Identifiable + + + - Tag: MultiLineDataSet + */ +public struct MultiLineDataSet: MultiDataSet { + + public let id : UUID + + public var dataSets : [LineDataSet] + + public init(dataSets: [LineDataSet]) { + self.id = UUID() + self.dataSets = dataSets + } + +} + diff --git a/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift new file mode 100644 index 00000000..b0f9876d --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift @@ -0,0 +1,69 @@ +// +// LineChartPoints.swift +// LineChart +// +// Created by Will Dale on 24/12/2020. +// + +import SwiftUI + +internal struct PointMarkers: ViewModifier where T: LineChartDataProtocol { + + @ObservedObject var chartData: T + + private let minValue : Double + private let range : Double + + internal init(chartData : T) { + self.chartData = chartData + self.minValue = chartData.getMinValue() + self.range = chartData.getRange() + } + internal func body(content: Content) -> some View { + ZStack { + content + if chartData.chartType.dataSetType == .single { + + let data = chartData as! LineChartData + PointsSubView(dataSets: data.dataSets, minValue: minValue, range: range, animation: chartData.chartStyle.globalAnimation, isFilled: chartData.isFilled) + } else if chartData.chartType.dataSetType == .multi { + + let data = chartData as! MultiLineChartData + ForEach(data.dataSets.dataSets, id: \.self) { dataSet in + PointsSubView(dataSets: dataSet, minValue: minValue, range: range, animation: chartData.chartStyle.globalAnimation, isFilled: chartData.isFilled) + } + } + } + } +} + +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 + + - Requires: + Chart Data to conform to LineChartDataProtocol. + - LineChartData + - MultiLineChartData + + # Available for: + - Line Chart + - Multi Line Chart + + # Unavailable for: + - Bar Chart + - Grouped Bar Chart + - Pie Chart + - Doughnut Chart + + - Parameter chartData: Chart data model. + - Returns: A new view containing the chart with point markers. + + - Tag: PointMarkers + */ + public func pointMarkers(chartData: T) -> some View { + self.modifier(PointMarkers(chartData: chartData)) + } +} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift similarity index 60% rename from Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift rename to Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift index 90eefe60..01e7e5b8 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift @@ -1,85 +1,12 @@ // -// LineChartPoints.swift -// LineChart +// PointsSubView.swift +// // -// Created by Will Dale on 24/12/2020. +// Created by Will Dale on 04/02/2021. // import SwiftUI - - -internal struct PointMarkers: ViewModifier where T: LineChartDataProtocol { - - @ObservedObject var chartData: T - - private let minValue : Double - private let range : Double - - internal init(chartData : T) { - self.chartData = chartData - - switch chartData.chartStyle.baseline { - case .minimumValue: - self.minValue = chartData.getMinValue() - self.range = chartData.getRange() - case .zero: - self.minValue = 0 - self.range = chartData.getMaxValue() - } - - - } - internal func body(content: Content) -> some View { - ZStack { - content - if chartData.chartType.dataSetType == .single { - - let data = chartData as! LineChartData - PointsSubView(dataSets: data.dataSets, minValue: minValue, range: range, animation: chartData.chartStyle.globalAnimation, isFilled: chartData.isFilled) - - } else if chartData.chartType.dataSetType == .multi { - - let data = chartData as! MultiLineChartData - ForEach(data.dataSets.dataSets, id: \.self) { dataSet in - PointsSubView(dataSets: dataSet, minValue: minValue, range: range, animation: chartData.chartStyle.globalAnimation, isFilled: chartData.isFilled) - } - } - } - } -} - -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 - - - Requires: - Chart Data to conform to LineChartDataProtocol. - - LineChartData - - MultiLineChartData - - # Available for: - - Line Chart - - Multi Line Chart - - # Unavailable for: - - Bar Chart - - Grouped Bar Chart - - Pie Chart - - Doughnut Chart - - - Parameter chartData: Chart data model. - - Returns: A new view containing the chart with point markers. - - - Tag: PointMarkers - */ - public func pointMarkers(chartData: T) -> some View { - self.modifier(PointMarkers(chartData: chartData)) - } -} - internal struct PointsSubView: View { private let dataSets: LineDataSet @@ -172,3 +99,4 @@ internal struct PointsSubView: View { } } + diff --git a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift index a03b905c..5f26f226 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift @@ -18,37 +18,3 @@ public struct ChartViewData { var hasYAxisLabels : Bool = false } - -/// Data model to pass view information internally so the layout can configure its self. -/// -/// # Reference -/// [CTChartDataPoint](x-source-tag://CTChartDataPoint) -/// -/// - Tag: InfoViewData -public struct InfoViewData { - /** - 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 : [DP] = [] - /** - Set specifier of data point readout - - Set from TouchOverlay - - Used by TitleBox - */ - var touchSpecifier : String = "%.0f" - -} diff --git a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift new file mode 100644 index 00000000..9c1313fe --- /dev/null +++ b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift @@ -0,0 +1,42 @@ +// +// InfoViewData.swift +// +// +// Created by Will Dale on 04/02/2021. +// + +import SwiftUI + +/// Data model to pass view information internally so the layout can configure its self. +/// +/// # Reference +/// [CTChartDataPoint](x-source-tag://CTChartDataPoint) +/// +/// - Tag: InfoViewData +public struct InfoViewData { + /** + 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 : [DP] = [] + /** + Set specifier of data point readout + + Set from TouchOverlay + + Used by TitleBox + */ + var touchSpecifier : String = "%.0f" + +} diff --git a/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift b/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift index 2be40f05..c720bb13 100644 --- a/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift @@ -94,6 +94,7 @@ extension LineAndBarChartData { return self.chartStyle.infoBoxPlacement } } + extension LineAndBarChartData where Set: SingleDataSet { public func getRange() -> Double { DataFunctions.dataSetRange(from: dataSets) diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols.swift b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift similarity index 99% rename from Sources/SwiftUICharts/Shared/Models/Protocols.swift rename to Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift index 9a4fe7f7..ec4a52fe 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift @@ -1,5 +1,5 @@ // -// File.swift +// SharedProtocols.swift // // // Created by Will Dale on 23/01/2021. diff --git a/Sources/SwiftUICharts/Shared/Models/GradientStop.swift b/Sources/SwiftUICharts/Shared/Types/GradientStop.swift similarity index 100% rename from Sources/SwiftUICharts/Shared/Models/GradientStop.swift rename to Sources/SwiftUICharts/Shared/Types/GradientStop.swift diff --git a/Sources/SwiftUICharts/Shared/Models/HashablePoint.swift b/Sources/SwiftUICharts/Shared/Types/HashablePoint.swift similarity index 100% rename from Sources/SwiftUICharts/Shared/Models/HashablePoint.swift rename to Sources/SwiftUICharts/Shared/Types/HashablePoint.swift diff --git a/Sources/SwiftUICharts/Shared/Models/Stroke.swift b/Sources/SwiftUICharts/Shared/Types/Stroke.swift similarity index 100% rename from Sources/SwiftUICharts/Shared/Models/Stroke.swift rename to Sources/SwiftUICharts/Shared/Types/Stroke.swift From 1608889cb3aa281fe2aa27c68d668a8d37080448 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 4 Feb 2021 21:00:41 +0000 Subject: [PATCH 022/152] Add documentation. --- .../BarChart/Models/BarChartData.swift | 155 ++++++++++++++++ .../BarChart/Models/BarChartDataPoint.swift | 77 ++++++-- .../BarChart/Models/BarChartProtocols.swift | 6 + .../BarChart/Models/BarChartStyle.swift | 82 +++++++-- .../BarChart/Models/BarDataSet.swift | 82 +++++++-- .../BarChart/Models/BarStyle.swift | 87 +++++---- .../BarChart/Models/CornerRadius.swift | 30 ++++ .../BarChart/Models/MultiBarChartData.swift | 168 ++++++++++++++++++ .../BarChart/Models/MultiBarDataSet.swift | 118 ++++++++++++ .../LineChart/Models/LineChartDataPoint.swift | 2 +- .../LineChart/Models/LineChartStyle.swift | 9 +- .../LineChart/Models/MultiLineDataSet.swift | 3 +- .../LineChart/Shapes/LineShape.swift | 1 - .../PieChart/Models/PieChartProtocols.swift | 16 +- .../SwiftUICharts/Shared/Extras/Enums.swift | 1 + .../Shared/Models/LegendData.swift | 16 +- .../LineAndBar/LineAndBarProtocols.swift | 18 +- 17 files changed, 763 insertions(+), 108 deletions(-) create mode 100644 Sources/SwiftUICharts/BarChart/Models/CornerRadius.swift create mode 100644 Sources/SwiftUICharts/BarChart/Models/MultiBarDataSet.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index c261a980..31613d83 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -7,6 +7,161 @@ import SwiftUI +/** + Data for drawing and styling a bar chart. + + This model contains all the data and styling information for a single data set bar chart. + + # Example + ``` + static func weekOfData() -> BarChartData { + + let data : BarDataSet = + 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", + pointStyle: PointStyle(), + style: BarStyle()) + + let metadata : ChartMetadata = ChartMetadata(title : "Test Data", + subtitle : "A weeks worth") + + let labels : [String] = ["Mon", "Thu", "Sun"] + + let chartStyle : BarChartStyle = BarChartStyle(infoBoxPlacement: .floating, + xAxisGridStyle : GridStyle(), + yAxisGridStyle : GridStyle(), + xAxisLabelPosition: .bottom, + xAxisLabelsFrom: .dataPoint, + yAxisLabelPosition: .leading, + yAxisNumberOfLabels: 5) + + return BarChartData(dataSets: data, + metadata: metadata, + xAxisLabels: labels, + chartStyle: chartStyle, + calculations: .none) + } + + ``` + + --- + + # Parts + ## BarChartDataPoint + ### Options + Common to all. + ``` + BarChartDataPoint(value: Double, + xAxisLabel: String?, + pointLabel: String?, + date: Date?, + ...) + ``` + + Single Colour. + ``` + BarChartDataPoint(... + colour: Color?) + ``` + + Gradient Colours. + ``` + BarChartDataPoint(... + colours: [Color]?, + startPoint: UnitPoint?, + endPoint: UnitPoint?) + ``` + + Gradient Colours with stop control. + ``` + BarChartDataPoint(... + stops: [GradientStop]?, + startPoint: UnitPoint?, + endPoint: UnitPoint?) + ``` + ## BarStyle + ### Options + ``` + BarStyle(barWidth : CGFloat, + cornerRadius : CornerRadius, + colourFrom : ColourFrom, + ...) + + BarStyle(... + colour: Color) + + BarStyle(... + colours: [Color], + startPoint: UnitPoint, + endPoint: UnitPoint) + + BarStyle(... + stops: [GradientStop], + startPoint: UnitPoint, + endPoint: UnitPoint) + ``` + + ## ChartMetadata + ``` + ChartMetadata(title: String?, subtitle: String?) + ``` + + ## BarChartStyle + ``` + BarChartStyle(infoBoxPlacement : InfoBoxPlacement, + xAxisGridStyle : GridStyle, + yAxisGridStyle : GridStyle, + xAxisLabelPosition : XAxisLabelPosistion, + xAxisLabelsFrom : LabelsFrom, + yAxisLabelPosition : YAxisLabelPosistion, + yAxisNumberOfLabels : Int, + globalAnimation : Animation) + ``` + + ### GridStyle + ``` + GridStyle(numberOfLines: Int, + lineColour : Color, + lineWidth : CGFloat, + dash : [CGFloat], + dashPhase : CGFloat) + ``` + + --- + + # Also See + - [BarDataSet](x-source-tag://BarDataSet) + - [BarChartDataPoint](x-source-tag://BarChartDataPoint) + - [BarStyle](x-source-tag://BarStyle) + - [ColourType](x-source-tag://ColourType) + - [CornerRadius](x-source-tag://CornerRadius) + - [ColourFrom](x-source-tag://ColourFrom) + - [GradientStop](x-source-tag://GradientStop) + - [Chart Metadata](x-source-tag://ChartMetadata) + - [BarChartStyle](x-source-tag://BarChartStyle) + - [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) + - [GridStyle](x-source-tag://GridStyle) + - [XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) + - [LabelsFrom](x-source-tag://LabelsFrom) + - [YAxisLabelPosistion](x-source-tag://YAxisLabelPosistion) + + # Conforms to + - ObservableObject + - Identifiable + - BarChartDataProtocol + - LineAndBarChartData + - ChartData + + - Tag: BarChartData + */ public class BarChartData: BarChartDataProtocol { public let id : UUID = UUID() diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift index 34f71184..9e5a5f6d 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift @@ -7,33 +7,76 @@ import SwiftUI -/// Data model for a data point. +/** + Data for a single data point. + + # Example + ``` + BarChartDataPoint(value: 20, + xAxisLabel: "M", + pointLabel: "Monday", + date: Date()) + ``` + + # Options + Common to all. + ``` + BarChartDataPoint(value: Double, + xAxisLabel: String?, + pointLabel: String?, + date: Date?, + ...) + ``` + + Single Colour. + ``` + BarChartDataPoint(... + colour: Color?) + ``` + + Gradient Colours. + ``` + BarChartDataPoint(... + colours: [Color]?, + startPoint: UnitPoint?, + endPoint: UnitPoint?) + ``` + + Gradient Colours with stop control. + ``` + BarChartDataPoint(... + stops: [GradientStop]?, + startPoint: UnitPoint?, + endPoint: UnitPoint?) + ``` + + # Also See + - [GradientStopt](x-source-tag://GradientStop) + + # Conforms to + - CTLineAndBarDataPoint + - CTChartDataPoint + - Hashable + - Identifiable + - CTColourStyle + + - Tag: BarChartDataPoint + */ public struct BarChartDataPoint: CTLineAndBarDataPoint, CTColourStyle { 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? + public var colour : Color? + public var colours : [Color]? + public var stops : [GradientStop]? + public var startPoint : UnitPoint? + public var endPoint : UnitPoint? // MARK: - init: single colour /// Data model for a single data point with colour for use with a bar chart. diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartProtocols.swift index c79cd88f..353f20c5 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartProtocols.swift @@ -20,6 +20,12 @@ import SwiftUI - Tag: BarChartDataProtocol */ public protocol BarChartDataProtocol: LineAndBarChartData where CTStyle: CTBarChartStyle { + /** + Data model conatining the style data for the chart. + + # Reference + [CTChartStyle](x-source-tag://CTChartStyle) + */ var chartStyle : CTStyle { get set } } diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift index 21fa6a4e..7768fd6e 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift @@ -7,28 +7,72 @@ 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), + + # Example + ``` + BarChartStyle(infoBoxPlacement: .header, + xAxisGridStyle : GridStyle(numberOfLines: 7, + lineColour : .gray, + lineWidth : 1, + dash : [8], + dashPhase : 0), + yAxisGridStyle : GridStyle(numberOfLines: 7, + lineColour : .gray, + lineWidth : 1, + dash : [8], + dashPhase : 0), + xAxisLabelPosition : .bottom, + xAxisLabelsFrom : .dataPoint, + yAxisLabelPosition : .leading, + yAxisNumberOfLabels : 5, + baseline : .minimumValue, + globalAnimation : .linear(duration: 1)) + ``` + + # Options + ``` + BarChartStyle(infoBoxPlacement : InfoBoxPlacement, + xAxisGridStyle : GridStyle, + yAxisGridStyle : GridStyle, + xAxisLabelPosition : XAxisLabelPosistion, + xAxisLabelsFrom : LabelsFrom, + yAxisLabelPosition : YAxisLabelPosistion, + yAxisNumberOfLabels : Int, + globalAnimation : Animation) + ``` + + --- + + # Also See + - [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) + - [GridStyle](x-source-tag://GridStyle) + - [XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) + - [LabelsFrom](x-source-tag://LabelsFrom) + - [YAxisLabelPosistion](x-source-tag://YAxisLabelPosistion) + + # Conforms to + - CTBarChartStyle + - CTLineAndBarChartStyle + - CTChartStyle + + - Tag: BarChartStyle + */ public struct BarChartStyle: CTBarChartStyle { - /// Placement of the information box that appears on touch input. - public var infoBoxPlacement : InfoBoxPlacement + public var infoBoxPlacement : InfoBoxPlacement + public var globalAnimation : Animation - /// 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 + public var xAxisGridStyle : GridStyle + public var yAxisGridStyle : GridStyle + public var xAxisLabelPosition : XAxisLabelPosistion + public var xAxisLabelsFrom : LabelsFrom + public var yAxisLabelPosition : YAxisLabelPosistion + public var yAxisNumberOfLabels : Int /// Model for controlling the overall aesthetic of the chart. /// - Parameters: diff --git a/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift index 8f2e8e4a..3475bb41 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift @@ -7,6 +7,70 @@ import SwiftUI +/** + Data set for a standard 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", + pointStyle : PointStyle(), + style : LineStyle()) + ``` + + # BarChartDataPoint + ``` + BarChartDataPoint(value : Double, + xAxisLabel : String?, + pointLabel : String?, + date : Date?) + ``` + + # BarStyle + ``` + BarStyle(barWidth : CGFloat, + cornerRadius : CornerRadius, + colourFrom : ColourFrom, + ...) + + BarStyle(... + colour: Color) + + BarStyle(... + colours: [Color], + startPoint: UnitPoint, + endPoint: UnitPoint) + + BarStyle(... + stops: [GradientStop], + startPoint: UnitPoint, + endPoint: UnitPoint) + ``` + --- + # Also See + - [BarChartDataPoint](x-source-tag://BarChartDataPoint) + - [BarStyle](x-source-tag://BarStyle) + - [CornerRadius](x-source-tag://CornerRadius) + - [ColourFrom](x-source-tag://ColourFrom) + - [GradientStop](x-source-tag://GradientStop) + + # Conforms to + - SingleDataSet + - DataSet + - Hashable + - Identifiable + + - Tag: BarDataSet + */ + public struct BarDataSet: SingleDataSet { public let id : UUID @@ -15,6 +79,12 @@ public struct BarDataSet: SingleDataSet { public var pointStyle : PointStyle public var style : BarStyle + /// Initialises a new data set for a Bar 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 drawin. public init(dataPoints : [BarChartDataPoint], legendTitle : String, pointStyle : PointStyle, @@ -30,15 +100,3 @@ public struct BarDataSet: SingleDataSet { public typealias ID = UUID public typealias Styling = BarStyle } - -public struct MultiBarDataSet: MultiDataSet { - - public let id : UUID - - public var dataSets : [BarDataSet] - - public init(dataSets: [BarDataSet]) { - self.id = UUID() - self.dataSets = dataSets - } -} diff --git a/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift b/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift index 0ac34036..a729a9a3 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift @@ -7,31 +7,70 @@ import SwiftUI -/// Model for controlling the aesthetic of the bar chart. +/** + Model for controlling the aesthetic of the line chart. + + # Example + ``` + BarStyle(barWidth : 0.5, + cornerRadius : CornerRadius(top: 15), + colourFrom : .barStyle, + colour : .blue) + ``` + + --- + + # Options + ``` + BarStyle(barWidth : CGFloat, + cornerRadius : CornerRadius, + colourFrom : ColourFrom, + ...) + + BarStyle(... + colour: Color) + + BarStyle(... + colours: [Color], + startPoint: UnitPoint, + endPoint: UnitPoint) + + BarStyle(... + stops: [GradientStop], + startPoint: UnitPoint, + endPoint: UnitPoint) + ``` + + --- + + # Also See + - [ColourType](x-source-tag://ColourType) + - [CornerRadius](x-source-tag://CornerRadius) + - [ColourFrom](x-source-tag://ColourFrom) + - [GradientStop](x-source-tag://GradientStop) + + # Conforms to + - CTColourStyle + - Hashable + + - Tag: BarStyle + */ public struct BarStyle: CTColourStyle, Hashable { - /// How much of the available width to use. 0 ..1 + /// 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. - 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? + + public var colourType : ColourType + public var colour : Color? + public var colours : [Color]? + public var stops : [GradientStop]? + public var startPoint : UnitPoint? + public var endPoint : UnitPoint? -// public var ignoreZero: Bool /// Bar Chart with single colour /// - Parameters: @@ -107,17 +146,3 @@ public struct BarStyle: CTColourStyle, Hashable { self.colourType = .gradientStops } } - -/// Corner radius of the bar shape. -public struct CornerRadius: Hashable { - - 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/CornerRadius.swift b/Sources/SwiftUICharts/BarChart/Models/CornerRadius.swift new file mode 100644 index 00000000..8975e21c --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/CornerRadius.swift @@ -0,0 +1,30 @@ +// +// CornerRadius.swift +// +// +// Created by Will Dale on 04/02/2021. +// + +import SwiftUI + +/** + Corner radius of the bar shape. + + --- + + # Conforms to + Hashable + + - Tag: CornerRadius + */ +public struct CornerRadius: Hashable { + + var top : CGFloat + var bottom : CGFloat + + /// Set the coner radius for the bar shapes + public init(top: CGFloat, bottom: CGFloat) { + self.top = top + self.bottom = bottom + } +} diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift index c1e4614d..2e9c3f6e 100644 --- a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift @@ -7,6 +7,174 @@ 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. + + # Example + ``` + static func makeData() -> MultiBarChartData { + + let data = MultiBarDataSet(dataSets: [ + BarDataSet(dataPoints: [ + BarChartDataPoint(value: 10, xAxisLabel: "1.1", pointLabel: "One One" , colour: .blue), + BarChartDataPoint(value: 20, xAxisLabel: "1.2", pointLabel: "One Two" , colour: .yellow), + BarChartDataPoint(value: 30, xAxisLabel: "1.3", pointLabel: "One Three", colour: .purple), + BarChartDataPoint(value: 40, xAxisLabel: "1.4", pointLabel: "One Four" , colour: .green)], + legendTitle: "One", + pointStyle: PointStyle(), + style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)), + BarDataSet(dataPoints: [ + BarChartDataPoint(value: 50, xAxisLabel: "2.1", pointLabel: "Two One" , colour: .blue), + BarChartDataPoint(value: 10, xAxisLabel: "2.2", pointLabel: "Two Two" , colour: .yellow), + BarChartDataPoint(value: 40, xAxisLabel: "2.3", pointLabel: "Two Three", colour: .purple), + BarChartDataPoint(value: 60, xAxisLabel: "2.3", pointLabel: "Two Three", colour: .green)], + legendTitle: "Two", + pointStyle: PointStyle(), + style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)), + BarDataSet(dataPoints: [ + BarChartDataPoint(value: 10, xAxisLabel: "3.1", pointLabel: "Three One" , colour: .blue), + BarChartDataPoint(value: 50, xAxisLabel: "3.2", pointLabel: "Three Two" , colour: .yellow), + BarChartDataPoint(value: 30, xAxisLabel: "3.3", pointLabel: "Three Three", colour: .purple), + BarChartDataPoint(value: 99, xAxisLabel: "3.4", pointLabel: "Three Four" , colour: .green)], + legendTitle: "Three", + pointStyle: PointStyle(), + style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)), + BarDataSet(dataPoints: [ + BarChartDataPoint(value: 80, xAxisLabel: "4.1", pointLabel: "Four One" , colour: .blue), + BarChartDataPoint(value: 10, xAxisLabel: "4.2", pointLabel: "Four Two" , colour: .yellow), + BarChartDataPoint(value: 20, xAxisLabel: "4.3", pointLabel: "Four Three", colour: .purple), + BarChartDataPoint(value: 50, xAxisLabel: "4.3", pointLabel: "Four Three", colour: .green)], + legendTitle: "Four", + pointStyle: PointStyle(), + style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)) + ]) + + return MultiBarChartData(dataSets: data, + metadata: ChartMetadata(title: "Hello", subtitle: "Bob"), + xAxisLabels: ["Hello"], + chartStyle: BarChartStyle(), + calculations: .none) + } + ``` + + --- + + # Parts + # BarDataSet + ``` + BarDataSet(dataPoints: [BarChartDataPoint], + legendTitle: String, + style: BarStyle) + ``` + ## BarChartDataPoint + ### Options + Common to all. + ``` + BarChartDataPoint(value: Double, + xAxisLabel: String?, + pointLabel: String?, + date: Date?, + ...) + ``` + + Single Colour. + ``` + BarChartDataPoint(... + colour: Color?) + ``` + + Gradient Colours. + ``` + BarChartDataPoint(... + colours: [Color]?, + startPoint: UnitPoint?, + endPoint: UnitPoint?) + ``` + + Gradient Colours with stop control. + ``` + BarChartDataPoint(... + stops: [GradientStop]?, + startPoint: UnitPoint?, + endPoint: UnitPoint?) + ``` + ## BarStyle + ### Options + ``` + BarStyle(barWidth : CGFloat, + cornerRadius : CornerRadius, + colourFrom : ColourFrom, + ...) + + BarStyle(... + colour: Color) + + BarStyle(... + colours: [Color], + startPoint: UnitPoint, + endPoint: UnitPoint) + + BarStyle(... + stops: [GradientStop], + startPoint: UnitPoint, + endPoint: UnitPoint) + ``` + + ## ChartMetadata + ``` + ChartMetadata(title: String?, subtitle: String?) + ``` + + ## BarChartStyle + ``` + BarChartStyle(infoBoxPlacement : InfoBoxPlacement, + xAxisGridStyle : GridStyle, + yAxisGridStyle : GridStyle, + xAxisLabelPosition : XAxisLabelPosistion, + xAxisLabelsFrom : LabelsFrom, + yAxisLabelPosition : YAxisLabelPosistion, + yAxisNumberOfLabels : Int, + globalAnimation : Animation) + ``` + + ### GridStyle + ``` + GridStyle(numberOfLines: Int, + lineColour : Color, + lineWidth : CGFloat, + dash : [CGFloat], + dashPhase : CGFloat) + ``` + + --- + + # Also See + - [BarDataSet](x-source-tag://BarDataSet) + - [BarChartDataPoint](x-source-tag://BarChartDataPoint) + - [BarStyle](x-source-tag://BarStyle) + - [ColourType](x-source-tag://ColourType) + - [CornerRadius](x-source-tag://CornerRadius) + - [ColourFrom](x-source-tag://ColourFrom) + - [GradientStop](x-source-tag://GradientStop) + - [Chart Metadata](x-source-tag://ChartMetadata) + - [BarChartStyle](x-source-tag://BarChartStyle) + - [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) + - [GridStyle](x-source-tag://GridStyle) + - [XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) + - [LabelsFrom](x-source-tag://LabelsFrom) + - [YAxisLabelPosistion](x-source-tag://YAxisLabelPosistion) + + # Conforms to + - ObservableObject + - Identifiable + - BarChartDataProtocol + - LineAndBarChartData + - ChartData + + - Tag: LineChartData + */ public class MultiBarChartData: BarChartDataProtocol { public let id : UUID = UUID() diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/MultiBarDataSet.swift new file mode 100644 index 00000000..77fff733 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/MultiBarDataSet.swift @@ -0,0 +1,118 @@ +// +// MultiBarDataSet.swift +// +// +// Created by Will Dale on 04/02/2021. +// + +import SwiftUI + +/** + Data set for a multi bar, bar charts. + + Contains information about each of bar sets within the chart. + + # Example + ``` + let data = MultiBarDataSet(dataSets: [ + BarDataSet(dataPoints: [ + BarChartDataPoint(value: 10, xAxisLabel: "1.1", pointLabel: "One One" , colour: .blue), + BarChartDataPoint(value: 20, xAxisLabel: "1.2", pointLabel: "One Two" , colour: .yellow), + BarChartDataPoint(value: 30, xAxisLabel: "1.3", pointLabel: "One Three", colour: .purple), + BarChartDataPoint(value: 40, xAxisLabel: "1.4", pointLabel: "One Four" , colour: .green)], + legendTitle: "One", + pointStyle: PointStyle(), + style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)), + BarDataSet(dataPoints: [ + BarChartDataPoint(value: 50, xAxisLabel: "2.1", pointLabel: "Two One" , colour: .blue), + BarChartDataPoint(value: 10, xAxisLabel: "2.2", pointLabel: "Two Two" , colour: .yellow), + BarChartDataPoint(value: 40, xAxisLabel: "2.3", pointLabel: "Two Three", colour: .purple), + BarChartDataPoint(value: 60, xAxisLabel: "2.3", pointLabel: "Two Three", colour: .green)], + legendTitle: "Two", + pointStyle: PointStyle(), + style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)), + BarDataSet(dataPoints: [ + BarChartDataPoint(value: 10, xAxisLabel: "3.1", pointLabel: "Three One" , colour: .blue), + BarChartDataPoint(value: 50, xAxisLabel: "3.2", pointLabel: "Three Two" , colour: .yellow), + BarChartDataPoint(value: 30, xAxisLabel: "3.3", pointLabel: "Three Three", colour: .purple), + BarChartDataPoint(value: 99, xAxisLabel: "3.4", pointLabel: "Three Four" , colour: .green)], + legendTitle: "Three", + pointStyle: PointStyle(), + style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)), + BarDataSet(dataPoints: [ + BarChartDataPoint(value: 80, xAxisLabel: "4.1", pointLabel: "Four One" , colour: .blue), + BarChartDataPoint(value: 10, xAxisLabel: "4.2", pointLabel: "Four Two" , colour: .yellow), + BarChartDataPoint(value: 20, xAxisLabel: "4.3", pointLabel: "Four Three", colour: .purple), + BarChartDataPoint(value: 50, xAxisLabel: "4.3", pointLabel: "Four Three", colour: .green)], + legendTitle: "Four", + style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)) + ]) + ``` + + # DataSet + ``` + BarDataSet(dataPoints: [BarChartDataPoint], + legendTitle: String, + style: BarStyle) + ``` + + + # BarChartDataPoint + ``` + BarChartDataPoint(value: Double, + xAxisLabel: String?, + pointLabel: String?, + date: Date?) + ``` + + # BarStyle + ``` + BarStyle(barWidth : CGFloat, + cornerRadius : CornerRadius, + colourFrom : ColourFrom, + ...) + + BarStyle(... + colour: Color) + + BarStyle(... + colours: [Color], + startPoint: UnitPoint, + endPoint: UnitPoint) + + BarStyle(... + stops: [GradientStop], + startPoint: UnitPoint, + endPoint: UnitPoint) + ``` + + --- + # Also See + - [BarDataSet](x-source-tag://BarDataSet) + - [BarChartDataPoint](x-source-tag://BarChartDataPoint) + - [BarStyle](x-source-tag://BarStyle) + - [CornerRadius](x-source-tag://CornerRadius) + - [ColourFrom](x-source-tag://ColourFrom) + - [GradientStop](x-source-tag://GradientStop) + + # Conforms to + - MultiDataSet + - DataSet + - Hashable + - Identifiable + + + - Tag: MultiBarDataSet + */ +public struct MultiBarDataSet: MultiDataSet { + + public let id : UUID + public var dataSets : [BarDataSet] + + /// Initialises a new data set for Multiline Line Chart. + public init(dataSets: [BarDataSet]) { + self.id = UUID() + self.dataSets = dataSets + } +} + diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift index 21c5c33f..58646505 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift @@ -15,7 +15,7 @@ import SwiftUI LineChartDataPoint(value: 20, xAxisLabel: "M", pointLabel: "Monday", - data: Date()) + date: Date()) ``` # Conforms to diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift index 10f86d58..5e4fc8b6 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift @@ -98,17 +98,14 @@ public struct LineChartStyle: CTLineChartStyle { baseline : Baseline = .minimumValue, globalAnimation : Animation = Animation.linear(duration: 1) ) { - self.infoBoxPlacement = infoBoxPlacement - self.xAxisGridStyle = xAxisGridStyle - self.yAxisGridStyle = yAxisGridStyle - + self.infoBoxPlacement = infoBoxPlacement + self.xAxisGridStyle = xAxisGridStyle + self.yAxisGridStyle = yAxisGridStyle self.xAxisLabelPosition = xAxisLabelPosition self.xAxisLabelsFrom = xAxisLabelsFrom self.yAxisLabelPosition = yAxisLabelPosition self.yAxisNumberOfLabels = yAxisNumberOfLabels - self.baseline = baseline - self.globalAnimation = globalAnimation } } diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineDataSet.swift index d7a6d643..c15897c4 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineDataSet.swift @@ -12,7 +12,7 @@ import SwiftUI Contains information about each of lines within the chart. - + # Example ``` let data = MultiLineDataSet(dataSets: [ LineDataSet(dataPoints: [ @@ -117,6 +117,7 @@ public struct MultiLineDataSet: MultiDataSet { public var dataSets : [LineDataSet] + /// Initialises a new data set for Multiline Line Chart. public init(dataSets: [LineDataSet]) { self.id = UUID() self.dataSets = dataSets diff --git a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift index 25b2f570..c9f7d4ed 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift @@ -12,7 +12,6 @@ internal struct LineShape: Shape { private let dataPoints : [LineChartDataPoint] /// Drawing style of the line 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 diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift index a3ef63ae..926b676a 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift @@ -18,10 +18,14 @@ import SwiftUI - Tag: PieAndDoughnutChartDataProtocol */ public protocol PieAndDoughnutChartDataProtocol: ChartData { + /// `associatedtype` to set the which `ChartStyle` to use. associatedtype CTStyle : CTPieAndDoughnutChartStyle /** - Protocol to set the styling data for the chart. + Data model conatining the style data for the chart. + + # Reference + [CTChartStyle](x-source-tag://CTChartStyle) */ var chartStyle : CTStyle { get set } } @@ -37,7 +41,10 @@ public protocol PieAndDoughnutChartDataProtocol: ChartData { public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol where CTStyle: CTPieChartStyle { /** - Protocol to set the styling data for the chart. + Data model conatining the style data for the chart. + + # Reference + [CTChartStyle](x-source-tag://CTChartStyle) */ var chartStyle : CTStyle { get set } } @@ -53,7 +60,10 @@ public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol where CTS public protocol DoughnutChartDataProtocol : PieAndDoughnutChartDataProtocol where CTStyle: CTDoughnutChartStyle { /** - Protocol to set the styling data for the chart. + Data model conatining the style data for the chart. + + # Reference + [CTChartStyle](x-source-tag://CTChartStyle) */ var chartStyle : CTStyle { get set } } diff --git a/Sources/SwiftUICharts/Shared/Extras/Enums.swift b/Sources/SwiftUICharts/Shared/Extras/Enums.swift index 9ea5c94f..41217862 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Enums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Enums.swift @@ -106,6 +106,7 @@ public enum LineType { 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 ``` diff --git a/Sources/SwiftUICharts/Shared/Models/LegendData.swift b/Sources/SwiftUICharts/Shared/Models/LegendData.swift index b59ea5aa..34d6b27c 100644 --- a/Sources/SwiftUICharts/Shared/Models/LegendData.swift +++ b/Sources/SwiftUICharts/Shared/Models/LegendData.swift @@ -12,31 +12,23 @@ import SwiftUI public struct LegendData: CTColourStyle, Hashable, Identifiable { public var id : UUID - + /// The type of chart being used. public var chartType : ChartType - /// Text to be displayed public var legend : String - /// Style of the stroke public var strokeStyle : Stroke? + /// Used to make sure the charts data legend is first + public let prioity : Int + 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? - /// Used to make sure the charts data legend is first - public let prioity : Int - /// Legend with single colour /// - Parameters: /// - legend: Text to be displayed diff --git a/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift b/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift index c720bb13..ae825121 100644 --- a/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift @@ -18,13 +18,16 @@ import SwiftUI */ public protocol LineAndBarChartData : ChartData { + /// Apple's `associatedtype` for outputting `some View`. associatedtype Body : View + /// `associatedtype` to set the which `ChartStyle` to use. associatedtype CTStyle : CTLineAndBarChartStyle /** 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. + 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 } /** @@ -38,14 +41,16 @@ public protocol LineAndBarChartData : ChartData { /** Array of strings for the labels on the X Axis instead of the labels in the data points. - To control where the labels should come from. Set [LabelsFrom](x-source-tag://LabelsFrom) in [ChartStyle](x-source-tag://CTChartStyle). + To control where the labels should come from. + Set [LabelsFrom](x-source-tag://LabelsFrom) in [ChartStyle](x-source-tag://CTChartStyle). */ var xAxisLabels: [String]? { get set } /** Displays a view for the labels on the X Axis. - Labels can come from either [CTChartDataPoint](x-source-tag://CTChartDataPoint) or [ChartData](x-source-tag://ChartData) + Labels can come from either [CTChartDataPoint](x-source-tag://CTChartDataPoint) + or [ChartData](x-source-tag://ChartData) - Returns: An `HStack` of `Text` containin x axis labels. @@ -56,7 +61,8 @@ public protocol LineAndBarChartData : ChartData { /** 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. + 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. @@ -180,7 +186,9 @@ public protocol CTLineAndBarChartStyle: CTChartStyle { */ public protocol CTLineAndBarDataPoint: CTChartDataPoint { - /// Data points label for the X axis. + /** + Data points label for the X axis. + */ var xAxisLabel : String? { get set } } From fe3d7f7409cf791e41745b46a30ec5d0e6de38d5 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 5 Feb 2021 18:19:57 +0000 Subject: [PATCH 023/152] Tidy up. --- .../PieChart/Models/PieChartDataPoint.swift | 4 +-- .../PieChart/Models/PieChartProtocols.swift | 15 ++++++++- .../PieChart/Models/PieDataSet.swift | 2 ++ .../PieChart/Views/DoughnutChart.swift | 32 +++++++++---------- .../PieChart/Views/PieChart.swift | 32 +++++++++---------- 5 files changed, 48 insertions(+), 37 deletions(-) diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartDataPoint.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartDataPoint.swift index a3066551..abff4c7a 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartDataPoint.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartDataPoint.swift @@ -17,8 +17,8 @@ public struct PieChartDataPoint: CTPieDataPoint { public var colour : Color - var startAngle : Double = 0 - var amount : Double = 0 + public var startAngle : Double = 0 + public var amount : Double = 0 public init(value : Double, xAxisLabel : String? = nil, diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift index 926b676a..c5025909 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift @@ -68,6 +68,16 @@ public protocol DoughnutChartDataProtocol : PieAndDoughnutChartDataProtocol wher var chartStyle : CTStyle { get set } } +public protocol CTMultiPieChartDataPoints: CTChartDataPoint { + +} + +public protocol CTMultiPieDataSet: DataSet { + +// var dataSets: + + +} // MARK: - Pie and Doughnut extension PieAndDoughnutChartDataProtocol { @@ -176,6 +186,9 @@ public protocol CTDoughnutChartStyle: CTPieAndDoughnutChartStyle { - Tag: CTPieDataPoint */ -public protocol CTPieDataPoint: CTChartDataPoint {} +public protocol CTPieDataPoint: CTChartDataPoint { + var startAngle : Double { get set } + var amount : Double { get set } +} diff --git a/Sources/SwiftUICharts/PieChart/Models/PieDataSet.swift b/Sources/SwiftUICharts/PieChart/Models/PieDataSet.swift index d271f7f6..2fa465f0 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieDataSet.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieDataSet.swift @@ -29,3 +29,5 @@ public struct PieDataSet: SingleDataSet { public typealias Styling = PieSegmentStyle public typealias DataPoint = PieChartDataPoint } + + diff --git a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift index 16398ace..7bf0f94a 100644 --- a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift @@ -18,23 +18,21 @@ public struct DoughnutChart: View where ChartData: DoughnutChartData } public var body: some View { - GeometryReader { geo in - 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) - .strokeBorder(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) - } - } + 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) + .strokeBorder(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) + } } } .animateOnAppear(using: chartData.chartStyle.globalAnimation) { diff --git a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift index e49e5fd7..811890f4 100644 --- a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift @@ -18,23 +18,21 @@ public struct PieChart: View where ChartData: PieChartData { } public var body: some View { - GeometryReader { geo in - 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) - } - } + 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) + } } } .animateOnAppear(using: chartData.chartStyle.globalAnimation) { From d4976c378b2331a85f6388663d4d9cc2de9c2a75 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 5 Feb 2021 18:20:37 +0000 Subject: [PATCH 024/152] Test sunburst chart. --- .../MultiLayer/MultiLayerPieChartData.swift | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift diff --git a/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift new file mode 100644 index 00000000..c59d96c3 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift @@ -0,0 +1,163 @@ +// +// MultiLayerPieChartData.swift +// +// +// Created by Will Dale on 05/02/2021. +// + +import SwiftUI + +public class MultiLayerPieChartData { + + @Published public var id : UUID = UUID() + @Published public var dataSets : MultiPieDataSet + @Published public var metadata : ChartMetadata? + @Published public var chartStyle : PieChartStyle + @Published public var legends : [LegendData] +// @Published public var infoView : InfoViewData + + public var noDataText: Text + public var chartType: (chartType: ChartType, dataSetType: DataSetType) + + public init(dataSets : MultiPieDataSet, + metadata : ChartMetadata? = nil, + chartStyle : PieChartStyle = PieChartStyle(), + 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: .multi) +// self.setupLegends() + + } + +} + +public struct MultiPieDataSet: Hashable, Identifiable { + + public let id : UUID + public var dataPoints : [MultiPieDataPoint] { + didSet { + let start = dataPoints.first?.startAngle ?? 0 + let amount = dataPoints.last?.amount ?? 0 + let end = start + amount + segmentWidth = end + } + } + + var segmentWidth : Double? + + /// Initialises a new data set for Multiline Line Chart. + public init(dataPoints: [MultiPieDataPoint]) { + self.id = UUID() + self.dataPoints = dataPoints + } +} + +public struct MultiPieDataPoint: Hashable, Identifiable { + + public var id : UUID = UUID() + public var value : Double + public var xAxisLabel : String? + public var pointDescription : String? + public var date : Date? + public var colour : Color + + public var dataSets : MultiPieDataSet? + + var startAngle : Double = 0 + var amount : Double = 0 + + public init(value : Double, + xAxisLabel : String? = nil, + pointDescription: String? = nil, + date : Date? = nil, + colour : Color = Color.red, + dataSets : MultiPieDataSet? = nil + ) { + self.value = value + self.xAxisLabel = xAxisLabel + self.pointDescription = pointDescription + self.date = date + self.colour = colour + + self.dataSets = dataSets + + } +} + +// MARK: - View + +public struct MultiLayerPieChart: View { + + let chartData : MultiLayerPieChartData = makeData() + + public init() {} + + public var body: some View { + + ZStack { + + ForEach(chartData.dataSets.dataPoints, id: \.self) { dataPoint in + +// PieSegmentShape(id: dataPoint.id, +// startAngle: dataPoint.startAngle, +// amount: dataPoint.amount) +// .fill(dataPoint.colour) + + if let bob = dataPoint.dataSets { + + ForEach(bob.dataPoints) { point in + DoughnutSegmentShape(id : UUID(), + startAngle : point.startAngle, + amount : point.amount) + .strokeBorder(point.colour, + lineWidth: 30) + } + + } + + } + } + } +} + +extension MultiLayerPieChart { + static func makeData() -> MultiLayerPieChartData { + + let data = MultiPieDataSet(dataPoints: [ + MultiPieDataPoint( + value: 10, + colour: Color(.gray), + dataSets: MultiPieDataSet(dataPoints: [ + MultiPieDataPoint(value: 20, + colour: .red), + MultiPieDataPoint(value: 20, + colour: .green)])), + + MultiPieDataPoint( + value: 40, + colour: Color(.darkGray), + dataSets: MultiPieDataSet(dataPoints: [ + MultiPieDataPoint(value: 20, + colour: .blue), + MultiPieDataPoint(value: 20, + colour: Color(.cyan))])), + MultiPieDataPoint( + value: 20, + colour: Color(.gray), + dataSets: MultiPieDataSet(dataPoints: [ + MultiPieDataPoint(value: 20, + colour: Color(.yellow)), + MultiPieDataPoint(value: 20, + colour: Color(.magenta))]) + )]) + + return MultiLayerPieChartData(dataSets: data, + noDataText: Text("Bob")) + } +} From b11f7b06b235949ba7af7cdf1ac95ca088391792 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 7 Feb 2021 12:15:01 +0000 Subject: [PATCH 025/152] Add option for POI marker labels. --- .../SwiftUICharts/Shared/Extras/Enums.swift | 18 +++ .../Shared/Extras/Extensions.swift | 23 ++- .../Shared/Shapes/DiamondShape.swift | 26 ++++ .../Shared/ViewModifiers/YAxisPOI.swift | 133 +++++++++++++----- 4 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 Sources/SwiftUICharts/Shared/Shapes/DiamondShape.swift diff --git a/Sources/SwiftUICharts/Shared/Extras/Enums.swift b/Sources/SwiftUICharts/Shared/Extras/Enums.swift index 41217862..6d04e20f 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Enums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Enums.swift @@ -264,3 +264,21 @@ 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) +} diff --git a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift index e13eb63f..f6deb6b7 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift @@ -15,6 +15,7 @@ extension View { else { self } } } +// https://fivestars.blog/swiftui/conditional-modifiers.html extension View { @ViewBuilder func `ifElse`(_ condition: Bool, @@ -42,10 +43,24 @@ extension View { } func animateOnDisappear(using animation: Animation = Animation.easeInOut(duration: 1), _ action: @escaping () -> Void) -> some View { - return onDisappear { - withAnimation(animation) { - action() - } + return onDisappear { + withAnimation(animation) { + action() } } + } +} + +extension Color { + 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 + } } diff --git a/Sources/SwiftUICharts/Shared/Shapes/DiamondShape.swift b/Sources/SwiftUICharts/Shared/Shapes/DiamondShape.swift new file mode 100644 index 00000000..43dbce8b --- /dev/null +++ b/Sources/SwiftUICharts/Shared/Shapes/DiamondShape.swift @@ -0,0 +1,26 @@ +// +// DiamondShape.swift +// +// +// Created by Will Dale on 07/02/2021. +// + +import SwiftUI + +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/Shared/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift index 9aebe54e..6f6559c4 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift @@ -17,48 +17,101 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { private let lineColour : Color private let strokeStyle : StrokeStyle + private let labelPosition : DisplayValue + 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, - lineColour : Color, - strokeStyle : StrokeStyle, - isAverage : Bool + internal init(chartData : T, + markerName : String, + markerValue : Double = 0, + labelPosition : DisplayValue, + labelBackground: Color, + lineColour : Color, + strokeStyle : StrokeStyle, + isAverage : Bool ) { self.chartData = chartData self.markerName = markerName self.lineColour = lineColour self.strokeStyle = strokeStyle + self.labelPosition = labelPosition + self.labelBackground = labelBackground + self.markerValue = isAverage ? chartData.getAverage() : markerValue self.maxValue = chartData.getMaxValue() - self.range = chartData.getRange() - self.minValue = chartData.getMinValue() + self.range = chartData.getRange() + self.minValue = chartData.getMinValue() } internal func body(content: Content) -> some View { ZStack { content // if chartData.isGreaterThanTwo { - Marker(value : markerValue, - range : range, - minValue : minValue, - maxValue : maxValue, - chartType : chartData.chartType.chartType) - .stroke(lineColour, style: strokeStyle) - .onAppear { - if !chartData.legends.contains(where: { $0.legend == markerName }) { // init twice - chartData.legends.append(LegendData(id : UUID(), - legend : markerName, - colour : lineColour, - strokeStyle : Stroke.strokeStyleToStroke(strokeStyle: strokeStyle), - prioity : 2, - chartType : .line)) - } + marker + valueLabel // } + } + } + + var marker: some View { + Marker(value : markerValue, + range : range, + minValue : minValue, + maxValue : maxValue, + chartType : chartData.chartType.chartType) + .stroke(lineColour, style: strokeStyle) + .onAppear { + if !chartData.legends.contains(where: { $0.legend == markerName }) { // init twice + chartData.legends.append(LegendData(id : UUID(), + legend : markerName, + colour : lineColour, + strokeStyle : Stroke.strokeStyleToStroke(strokeStyle: strokeStyle), + prioity : 2, + chartType : .line)) + } + } + } + + var valueLabel: some View { + GeometryReader { geo in + + let y = geo.size.height / CGFloat(range) + let pointY = (CGFloat(markerValue - minValue) * -y) + geo.size.height + + switch labelPosition { + case .none: + + EmptyView() + + case .yAxis(specifier: let specifier): + + Text("\(markerValue, 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("\(markerValue, specifier: specifier)") + .font(.caption) + .padding() + .background(labelBackground) + .clipShape(DiamondShape()) + .overlay(DiamondShape() + .stroke(lineColour, style: strokeStyle) + ) + .position(x: geo.size.width / 2, y: pointY) } } } @@ -109,15 +162,19 @@ extension View { public func yAxisPOI(chartData : T, markerName : String, markerValue : Double, - lineColour : Color = Color(.blue), - strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round, miterLimit: 10, dash: [CGFloat](), dashPhase: 0) + labelPosition : DisplayValue = .center(specifier: "%.0f"), + 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, - lineColour : lineColour, - strokeStyle : strokeStyle, - isAverage : false)) + self.modifier(YAxisPOI(chartData : chartData, + markerName : markerName, + markerValue : markerValue, + labelPosition : labelPosition, + labelBackground: labelBackground, + lineColour : lineColour, + strokeStyle : strokeStyle, + isAverage : false)) } @@ -164,13 +221,17 @@ extension View { */ public func averageLine(chartData : T, markerName : String = "Average", + labelPosition : DisplayValue = .yAxis(specifier: "%.0f"), + 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, - lineColour : lineColour, - strokeStyle : strokeStyle, - isAverage : true)) + self.modifier(YAxisPOI(chartData : chartData, + markerName : markerName, + labelPosition : labelPosition, + labelBackground: labelBackground, + lineColour : lineColour, + strokeStyle : strokeStyle, + isAverage : true)) } } From 6582eb5416993d30f6673af2bbcf52f053c277e6 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Mon, 8 Feb 2021 08:56:11 +0000 Subject: [PATCH 026/152] Remove optional from ChartMetadata --- README.md | 2 +- .../BarChart/Models/BarChartData.swift | 6 ++--- .../BarChart/Models/MultiBarChartData.swift | 6 ++--- .../LineChart/Models/LineChartData.swift | 6 ++--- .../LineChart/Models/MultiLineChartData.swift | 6 ++--- .../Models/Doughnut/DoughnutChartData.swift | 4 ++-- .../MultiLayer/MultiLayerPieChartData.swift | 4 ++-- .../PieChart/Models/Pie/PieChartData.swift | 4 ++-- .../Shared/Models/ChartMetadata.swift | 24 +++++++++++++------ .../Shared/Models/SharedProtocols.swift | 2 +- 10 files changed, 37 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 5da61500..732399ed 100644 --- a/README.md +++ b/README.md @@ -422,7 +422,7 @@ The ChartData type is where the majority of the configuration is done. The only ```swift ChartData(dataPoints : [ChartDataPoint], - metadata : ChartMetadata?, + metadata : ChartMetadata, xAxisLabels : [String]?, chartStyle : ChartStyle, lineStyle : LineStyle, diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index 31613d83..16b112d1 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -167,7 +167,7 @@ public class BarChartData: BarChartDataProtocol { public let id : UUID = UUID() @Published public var dataSets : BarDataSet - @Published public var metadata : ChartMetadata? + @Published public var metadata : ChartMetadata @Published public var xAxisLabels : [String]? @Published public var chartStyle : BarChartStyle @Published public var legends : [LegendData] @@ -178,7 +178,7 @@ public class BarChartData: BarChartDataProtocol { public var chartType : (chartType: ChartType, dataSetType: DataSetType) public init(dataSets : BarDataSet, - metadata : ChartMetadata? = nil, + metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : BarChartStyle = BarChartStyle(), calculations: CalculationType = .none @@ -194,7 +194,7 @@ public class BarChartData: BarChartDataProtocol { } public init(dataSets : BarDataSet, - metadata : ChartMetadata? = nil, + metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : BarChartStyle = BarChartStyle(), customCalc : @escaping ([BarChartDataPoint]) -> [BarChartDataPoint]? diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift index 2e9c3f6e..119b603b 100644 --- a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift @@ -180,7 +180,7 @@ public class MultiBarChartData: BarChartDataProtocol { public let id : UUID = UUID() @Published public var dataSets : MultiBarDataSet - @Published public var metadata : ChartMetadata? + @Published public var metadata : ChartMetadata @Published public var xAxisLabels : [String]? @Published public var chartStyle : BarChartStyle @Published public var legends : [LegendData] @@ -191,7 +191,7 @@ public class MultiBarChartData: BarChartDataProtocol { public var chartType : (chartType: ChartType, dataSetType: DataSetType) public init(dataSets : MultiBarDataSet, - metadata : ChartMetadata? = nil, + metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : BarChartStyle = BarChartStyle(), calculations: CalculationType = .none @@ -207,7 +207,7 @@ public class MultiBarChartData: BarChartDataProtocol { } public init(dataSets : MultiBarDataSet, - metadata : ChartMetadata? = nil, + metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : BarChartStyle = BarChartStyle(), customCalc : @escaping ([BarChartDataPoint]) -> [BarChartDataPoint]? diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index abcdedf4..d59f5fdf 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -154,7 +154,7 @@ public class LineChartData: LineChartDataProtocol { public let id : UUID = UUID() @Published public var dataSets : LineDataSet - @Published public var metadata : ChartMetadata? + @Published public var metadata : ChartMetadata @Published public var xAxisLabels : [String]? @Published public var chartStyle : LineChartStyle @Published public var legends : [LegendData] @@ -179,7 +179,7 @@ public class LineChartData: LineChartDataProtocol { /// - chartStyle: The style data for the aesthetic of the chart. /// - calculations: Addition calculations that can be performed on the data set before drawing. public init(dataSets : LineDataSet, - metadata : ChartMetadata? = nil, + metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : LineChartStyle = LineChartStyle(), calculations: CalculationType = .none @@ -208,7 +208,7 @@ public class LineChartData: LineChartDataProtocol { /// - chartStyle: The style data for the aesthetic of the chart. /// - customCalc: Custom calculations that can be performed on the data set before drawing. public init(dataSets : LineDataSet, - metadata : ChartMetadata? = nil, + metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : LineChartStyle = LineChartStyle(), customCalc : @escaping ([LineChartDataPoint]) -> [LineChartDataPoint]? diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift index 4553195f..0129dc67 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -170,7 +170,7 @@ public class MultiLineChartData: LineChartDataProtocol { @Published public var dataSets : MultiLineDataSet /// Data model containing: the charts Title, the charts Subtitle and the Line Legend. - @Published public var metadata : ChartMetadata? + @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]? @@ -192,7 +192,7 @@ public class MultiLineChartData: LineChartDataProtocol { public var chartType : (chartType: ChartType, dataSetType: DataSetType) public init(dataSets : MultiLineDataSet, - metadata : ChartMetadata? = nil, + metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : LineChartStyle = LineChartStyle(), calculations: CalculationType = .none @@ -208,7 +208,7 @@ public class MultiLineChartData: LineChartDataProtocol { } public init(dataSets : MultiLineDataSet, - metadata : ChartMetadata? = nil, + metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : LineChartStyle = LineChartStyle(), customCalc : @escaping ([LineChartDataPoint]) -> [LineChartDataPoint]? diff --git a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift index a0f92bb0..3d7b9d38 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift @@ -11,7 +11,7 @@ public class DoughnutChartData: DoughnutChartDataProtocol { @Published public var id : UUID = UUID() @Published public var dataSets : PieDataSet - @Published public var metadata : ChartMetadata? + @Published public var metadata : ChartMetadata @Published public var chartStyle : DoughnutChartStyle @Published public var legends : [LegendData] @Published public var infoView : InfoViewData @@ -20,7 +20,7 @@ public class DoughnutChartData: DoughnutChartDataProtocol { public var chartType: (chartType: ChartType, dataSetType: DataSetType) public init(dataSets : PieDataSet, - metadata : ChartMetadata? = nil, + metadata : ChartMetadata, chartStyle : DoughnutChartStyle = DoughnutChartStyle(), noDataText : Text ) { diff --git a/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift index c59d96c3..efa8246f 100644 --- a/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift @@ -11,7 +11,7 @@ public class MultiLayerPieChartData { @Published public var id : UUID = UUID() @Published public var dataSets : MultiPieDataSet - @Published public var metadata : ChartMetadata? + @Published public var metadata : ChartMetadata @Published public var chartStyle : PieChartStyle @Published public var legends : [LegendData] // @Published public var infoView : InfoViewData @@ -20,7 +20,7 @@ public class MultiLayerPieChartData { public var chartType: (chartType: ChartType, dataSetType: DataSetType) public init(dataSets : MultiPieDataSet, - metadata : ChartMetadata? = nil, + metadata : ChartMetadata = ChartMetadata(), chartStyle : PieChartStyle = PieChartStyle(), noDataText : Text ) { diff --git a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift index 082c4983..08d37272 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift @@ -11,7 +11,7 @@ public class PieChartData: PieChartDataProtocol { @Published public var id : UUID = UUID() @Published public var dataSets : PieDataSet - @Published public var metadata : ChartMetadata? + @Published public var metadata : ChartMetadata @Published public var chartStyle : PieChartStyle @Published public var legends : [LegendData] @Published public var infoView : InfoViewData @@ -20,7 +20,7 @@ public class PieChartData: PieChartDataProtocol { public var chartType: (chartType: ChartType, dataSetType: DataSetType) public init(dataSets : PieDataSet, - metadata : ChartMetadata? = nil, + metadata : ChartMetadata, chartStyle : PieChartStyle = PieChartStyle(), noDataText : Text ) { diff --git a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift index 191590c8..8badb1e5 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift @@ -5,7 +5,7 @@ // Created by Will Dale on 03/01/2021. // -import Foundation +import SwiftUI /** Data model for the chart's metadata @@ -21,18 +21,28 @@ import Foundation */ public struct ChartMetadata { /// The charts title - var title : String? + var title : String /// The charts subtitle - var subtitle : String? + var subtitle : String + /// Color of the title + var titleColour : Color + /// Color of the subtitle + var subtitleColour: Color /// Model to hold the metadata for the chart. /// - Parameters: /// - title: The charts title /// - subtitle: The charts subtitle - public init(title : String? = nil, - subtitle : 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.title = title + self.subtitle = subtitle + self.titleColour = titleColour + self.subtitleColour = subtitleColour } } diff --git a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift index ec4a52fe..5886ed1c 100644 --- a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift @@ -35,7 +35,7 @@ public protocol ChartData: ObservableObject, Identifiable { # Reference [ChartMetadata](x-source-tag://ChartMetadata) */ - var metadata: ChartMetadata? { get set } + var metadata: ChartMetadata { get set } /** Array of `LegendData` to populate the chart legend. From 22e92a92c562e75e21f323d78a2e596754aadaeb Mon Sep 17 00:00:00 2001 From: Will Dale Date: Mon, 8 Feb 2021 11:16:21 +0000 Subject: [PATCH 027/152] Open colour choices to API. --- .../BarChart/Models/BarChartStyle.swift | 48 ++++++++--- .../LineChart/Models/LineChartStyle.swift | 50 ++++++++--- .../Models/Doughnut/DoughnutChartStyle.swift | 25 ++++-- .../PieChart/Models/Pie/PieChartStyle.swift | 20 +++-- .../PieChart/Models/PieChartProtocols.swift | 4 +- .../Shared/Extras/AddLegends.swift | 83 ------------------- .../Shared/Models/InfoViewData.swift | 5 +- .../LineAndBar/LineAndBarProtocols.swift | 39 +++++---- .../Shared/Models/SharedProtocols.swift | 32 ++++++- .../Shared/ViewModifiers/HeaderBox.swift | 35 +++----- .../Shared/ViewModifiers/Legends.swift | 8 +- .../Shared/ViewModifiers/TouchOverlay.swift | 28 ++++--- .../Shared/ViewModifiers/XAxisLabels.swift | 2 + .../Shared/ViewModifiers/YAxisLabels.swift | 1 + .../Shared/ViewModifiers/YAxisPOI.swift | 39 +++++++-- .../Shared/Views/LegendView.swift | 8 +- .../Shared/Views/TouchOverlayBox.swift | 17 +++- 17 files changed, 248 insertions(+), 196 deletions(-) delete mode 100644 Sources/SwiftUICharts/Shared/Extras/AddLegends.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift index 7768fd6e..f19b062e 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift @@ -64,43 +64,65 @@ import SwiftUI */ public struct BarChartStyle: CTBarChartStyle { - public var infoBoxPlacement : InfoBoxPlacement - public var globalAnimation : Animation + public var infoBoxPlacement : InfoBoxPlacement + public var infoBoxValueColour : Color + public var infoBoxDescriptionColor : Color public var xAxisGridStyle : GridStyle - public var yAxisGridStyle : GridStyle public var xAxisLabelPosition : XAxisLabelPosistion + public var xAxisLabelColour : Color public var xAxisLabelsFrom : LabelsFrom + + public var yAxisGridStyle : GridStyle public var yAxisLabelPosition : YAxisLabelPosistion + public var yAxisLabelColour : Color public var yAxisNumberOfLabels : Int + 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. + /// - infoBoxDescriptionColor: Colour of the description part of the touch info. /// - 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. /// - 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 + /// - 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. /// - globalAnimation: Gobal control of animations. - public init(infoBoxPlacement : InfoBoxPlacement = .floating, + public init(infoBoxPlacement : InfoBoxPlacement = .floating, + infoBoxValueColour : Color = Color.primary, + infoBoxDescriptionColor : Color = Color.primary, + xAxisGridStyle : GridStyle = GridStyle(), - yAxisGridStyle : GridStyle = GridStyle(), xAxisLabelPosition : XAxisLabelPosistion = .bottom, + xAxisLabelColour : Color = Color.primary, xAxisLabelsFrom : LabelsFrom = .dataPoint, + + yAxisGridStyle : GridStyle = GridStyle(), yAxisLabelPosition : YAxisLabelPosistion = .leading, + yAxisLabelColour : Color = Color.primary, yAxisNumberOfLabels : Int = 10, + globalAnimation : Animation = Animation.linear(duration: 1) ) { - self.infoBoxPlacement = infoBoxPlacement - self.xAxisGridStyle = xAxisGridStyle - self.yAxisGridStyle = yAxisGridStyle - + self.infoBoxPlacement = infoBoxPlacement + self.infoBoxValueColour = infoBoxValueColour + self.infoBoxDescriptionColor = infoBoxDescriptionColor + + self.xAxisGridStyle = xAxisGridStyle self.xAxisLabelPosition = xAxisLabelPosition + self.xAxisLabelColour = xAxisLabelColour self.xAxisLabelsFrom = xAxisLabelsFrom + + self.yAxisGridStyle = yAxisGridStyle self.yAxisLabelPosition = yAxisLabelPosition self.yAxisNumberOfLabels = yAxisNumberOfLabels + self.yAxisLabelColour = yAxisLabelColour self.globalAnimation = globalAnimation } diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift index 5e4fc8b6..3b74166f 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift @@ -65,46 +65,72 @@ import SwiftUI */ public struct LineChartStyle: CTLineChartStyle { - public var infoBoxPlacement : InfoBoxPlacement - public var globalAnimation : Animation - + public var infoBoxPlacement : InfoBoxPlacement + public var infoBoxValueColour : Color + public var infoBoxDescriptionColor : Color + public var xAxisGridStyle : GridStyle - public var yAxisGridStyle : GridStyle public var xAxisLabelPosition : XAxisLabelPosistion + public var xAxisLabelColour : Color public var xAxisLabelsFrom : LabelsFrom + + public var yAxisGridStyle : GridStyle public var yAxisLabelPosition : YAxisLabelPosistion + public var yAxisLabelColour : Color public var yAxisNumberOfLabels : Int public var baseline : Baseline + 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. + /// - infoBoxDescriptionColor: Colour of the description part of the touch info. + /// /// - 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. + /// /// - 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 + /// - 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. + /// /// - baseline: Whether the chart is drawn from baseline of zero or the minimum datapoint value. /// - globalAnimation: Gobal control of animations. - public init(infoBoxPlacement : InfoBoxPlacement = .floating, + public init(infoBoxPlacement : InfoBoxPlacement = .floating, + infoBoxValueColour : Color = Color.primary, + infoBoxDescriptionColor : Color = Color.primary, + xAxisGridStyle : GridStyle = GridStyle(), - yAxisGridStyle : GridStyle = GridStyle(), xAxisLabelPosition : XAxisLabelPosistion = .bottom, + xAxisLabelColour : Color = Color.primary, xAxisLabelsFrom : LabelsFrom = .dataPoint, + + yAxisGridStyle : GridStyle = GridStyle(), yAxisLabelPosition : YAxisLabelPosistion = .leading, + yAxisLabelColour : Color = Color.primary, yAxisNumberOfLabels : Int = 10, + baseline : Baseline = .minimumValue, globalAnimation : Animation = Animation.linear(duration: 1) ) { - self.infoBoxPlacement = infoBoxPlacement + self.infoBoxPlacement = infoBoxPlacement + self.infoBoxValueColour = infoBoxValueColour + self.infoBoxDescriptionColor = infoBoxDescriptionColor + self.xAxisGridStyle = xAxisGridStyle - self.yAxisGridStyle = yAxisGridStyle self.xAxisLabelPosition = xAxisLabelPosition self.xAxisLabelsFrom = xAxisLabelsFrom + self.xAxisLabelColour = xAxisLabelColour + + self.yAxisGridStyle = yAxisGridStyle self.yAxisLabelPosition = yAxisLabelPosition self.yAxisNumberOfLabels = yAxisNumberOfLabels + self.yAxisLabelColour = yAxisLabelColour + self.baseline = baseline self.globalAnimation = globalAnimation } diff --git a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartStyle.swift index 1efffde2..d4a6ae1b 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartStyle.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartStyle.swift @@ -10,10 +10,10 @@ import SwiftUI /// Model for controlling the overall aesthetic of the chart. public struct DoughnutChartStyle: CTDoughnutChartStyle { - /// Placement of the information box that appears on touch input. - public var infoBoxPlacement : InfoBoxPlacement + public var infoBoxPlacement : InfoBoxPlacement + public var infoBoxValueColour : Color + public var infoBoxDescriptionColor : Color - /// Gobal control of animations. public var globalAnimation : Animation public var strokeWidth : CGFloat @@ -21,13 +21,20 @@ public struct DoughnutChartStyle: CTDoughnutChartStyle { /// 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. + /// - infoBoxDescriptionColor: Colour of the description part of the touch info. /// - globalAnimation: Gobal control of animations. - public init(infoBoxPlacement : InfoBoxPlacement = .floating, - globalAnimation : Animation = Animation.linear(duration: 1), - strokeWidth : CGFloat = 30 + /// - strokeWidth: Width / Delta of the Doughnut Chart + public init(infoBoxPlacement : InfoBoxPlacement = .floating, + infoBoxValueColour : Color = Color.primary, + infoBoxDescriptionColor : Color = Color.primary, + globalAnimation : Animation = Animation.linear(duration: 1), + strokeWidth : CGFloat = 30 ) { - self.infoBoxPlacement = infoBoxPlacement - self.globalAnimation = globalAnimation - self.strokeWidth = strokeWidth + self.infoBoxPlacement = infoBoxPlacement + self.infoBoxValueColour = infoBoxValueColour + self.infoBoxDescriptionColor = infoBoxDescriptionColor + self.globalAnimation = globalAnimation + self.strokeWidth = strokeWidth } } diff --git a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartStyle.swift index efff3e3b..cc149e05 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartStyle.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartStyle.swift @@ -10,21 +10,27 @@ import SwiftUI /// Model for controlling the overall aesthetic of the chart. public struct PieChartStyle: CTPieChartStyle { - /// Placement of the information box that appears on touch input. - public var infoBoxPlacement : InfoBoxPlacement + public var infoBoxPlacement : InfoBoxPlacement + public var infoBoxValueColour : Color + public var infoBoxDescriptionColor : Color - /// 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. + /// - infoBoxValueColour: Colour of the value part of the touch info. + /// - infoBoxDescriptionColor: Colour of the description part of the touch info. /// - globalAnimation: Gobal control of animations. - public init(infoBoxPlacement : InfoBoxPlacement = .floating, - globalAnimation : Animation = Animation.linear(duration: 1) + public init(infoBoxPlacement : InfoBoxPlacement = .floating, + infoBoxValueColour : Color = Color.primary, + infoBoxDescriptionColor : Color = Color.primary, + globalAnimation : Animation = Animation.linear(duration: 1) ) { - self.infoBoxPlacement = infoBoxPlacement - self.globalAnimation = globalAnimation + self.infoBoxPlacement = infoBoxPlacement + self.infoBoxValueColour = infoBoxValueColour + self.infoBoxDescriptionColor = infoBoxDescriptionColor + self.globalAnimation = globalAnimation } } diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift index c5025909..80baa65b 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift @@ -17,9 +17,7 @@ import SwiftUI - Tag: PieAndDoughnutChartDataProtocol */ -public protocol PieAndDoughnutChartDataProtocol: ChartData { - /// `associatedtype` to set the which `ChartStyle` to use. - associatedtype CTStyle : CTPieAndDoughnutChartStyle +public protocol PieAndDoughnutChartDataProtocol: ChartData where CTStyle : CTPieAndDoughnutChartStyle{ /** Data model conatining the style data for the chart. diff --git a/Sources/SwiftUICharts/Shared/Extras/AddLegends.swift b/Sources/SwiftUICharts/Shared/Extras/AddLegends.swift deleted file mode 100644 index da6ff37c..00000000 --- a/Sources/SwiftUICharts/Shared/Extras/AddLegends.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// Legends.swift -// -// -// Created by Will Dale on 26/01/2021. -// - -import SwiftUI - -//internal struct AddLegends { -// static func setupLine(chartData: inout T, dataSet: LineDataSet) { -// if dataSet.style.colourType == .colour, -// let colour = dataSet.style.colour -// { -// chartData.legends.append(LegendData(legend : dataSet.legendTitle, -// colour : colour, -// strokeStyle: dataSet.style.strokeStyle, -// prioity : 1, -// chartType : .line)) -// -// } else if dataSet.style.colourType == .gradientColour, -// let colours = dataSet.style.colours -// { -// chartData.legends.append(LegendData(legend : dataSet.legendTitle, -// colours : colours, -// startPoint : .leading, -// endPoint : .trailing, -// strokeStyle: dataSet.style.strokeStyle, -// prioity : 1, -// chartType : .line)) -// -// } else if dataSet.style.colourType == .gradientStops, -// let stops = dataSet.style.stops -// { -// chartData.legends.append(LegendData(legend : dataSet.legendTitle, -// stops : stops, -// startPoint : .leading, -// endPoint : .trailing, -// strokeStyle: dataSet.style.strokeStyle, -// prioity : 1, -// chartType : .line)) -// } -//// chartData.viewData.chartType = .line -// } -// -// static func setupBar(chartData: inout T, dataSet: BarDataSet) { -// -// switch chartData.dataSets.style.colourFrom { -// case .barStyle: -// if dataSet.style.colourType == .colour, -// let colour = dataSet.style.colour -// { -// chartData.legends.append(LegendData(legend : dataSet.legendTitle, -// colour : colour, -// strokeStyle: nil, -// prioity : 1, -// chartType : .bar)) -// } else if dataSet.style.colourType == .gradientColour, -// let colours = dataSet.style.colours -// { -// chartData.legends.append(LegendData(legend : dataSet.legendTitle, -// colours : colours, -// startPoint : .leading, -// endPoint : .trailing, -// strokeStyle: nil, -// prioity : 1, -// chartType : .bar)) -// } else if dataSet.style.colourType == .gradientStops, -// let stops = dataSet.style.stops -// { -// chartData.legends.append(LegendData(legend : dataSet.legendTitle, -// stops : stops, -// startPoint : .leading, -// endPoint : .trailing, -// strokeStyle: nil, -// prioity : 1, -// chartType : .bar)) -// } -// case .dataPoints: -// Text("") -// } -// } -//} diff --git a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift index 9c1313fe..aa1ace94 100644 --- a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift @@ -14,6 +14,7 @@ import SwiftUI /// /// - Tag: InfoViewData public struct InfoViewData { + /** Is there currently input (touch or click) on the chart @@ -22,6 +23,7 @@ public struct InfoViewData { Used by TitleBox */ var isTouchCurrent : Bool = false + /** Closest data point to input @@ -30,6 +32,7 @@ public struct InfoViewData { Used by TitleBox */ var touchOverlayInfo : [DP] = [] + /** Set specifier of data point readout @@ -38,5 +41,5 @@ public struct InfoViewData { Used by TitleBox */ var touchSpecifier : String = "%.0f" - + } diff --git a/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift b/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift index ae825121..ba99065e 100644 --- a/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift @@ -16,12 +16,10 @@ import SwiftUI - Tag: LineAndBarChartData */ -public protocol LineAndBarChartData : ChartData { +public protocol LineAndBarChartData : ChartData where CTStyle: CTLineAndBarChartStyle { /// Apple's `associatedtype` for outputting `some View`. associatedtype Body : View - /// `associatedtype` to set the which `ChartStyle` to use. - associatedtype CTStyle : CTLineAndBarChartStyle /** Data model to hold data about the Views layout. @@ -30,13 +28,7 @@ public protocol LineAndBarChartData : ChartData { axis labels so they can configure thier layouts appropriately. */ var viewData: ChartViewData { get set } - /** - Data model conatining the style data for the chart. - - # Reference - [CTChartStyle](x-source-tag://CTChartStyle) - */ - var chartStyle: CTStyle { get set } + /** Array of strings for the labels on the X Axis instead of the labels in the data points. @@ -131,6 +123,7 @@ extension LineAndBarChartData where Set: MultiDataSet { } // MARK: - Style + /** A protocol to extend functionality of `CTChartStyle` specifically for Line and Bar Charts. @@ -145,18 +138,17 @@ public protocol CTLineAndBarChartStyle: CTChartStyle { var xAxisGridStyle: GridStyle { get set } /** - Style of the horizontal lines breaking up the chart. + Location of the X axis labels - Top or Bottom - [See GridStyle](x-source-tag://GridStyle) + [See XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) */ - var yAxisGridStyle: GridStyle { get set } + var xAxisLabelPosition: XAxisLabelPosistion { get set } /** - Location of the X axis labels - Top or Bottom + Text Colour for the labels on the X axis. - [See XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) */ - var xAxisLabelPosition: XAxisLabelPosistion { get set } + var xAxisLabelColour: Color { get set } /** Where the label data come from. DataPoint or ChartData @@ -165,6 +157,14 @@ public protocol CTLineAndBarChartStyle: CTChartStyle { */ var xAxisLabelsFrom: LabelsFrom { get set } + + /** + Style of the horizontal lines breaking up the chart. + + [See GridStyle](x-source-tag://GridStyle) + */ + var yAxisGridStyle: GridStyle { get set } + /** Location of the X axis labels - Leading or Trailing @@ -172,10 +172,17 @@ public protocol CTLineAndBarChartStyle: CTChartStyle { */ 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 } + } // MARK: DataPoints diff --git a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift index 5886ed1c..444d2446 100644 --- a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift @@ -20,6 +20,8 @@ public protocol ChartData: ObservableObject, Identifiable { associatedtype Set : DataSet associatedtype DataPoint: CTChartDataPoint + associatedtype CTStyle : CTChartStyle + var id: ID { get } /** @@ -49,6 +51,14 @@ public protocol ChartData: ObservableObject, Identifiable { */ var infoView: InfoViewData { get set } + /** + Data model conatining the style data for the chart. + + # Reference + [CTChartStyle](x-source-tag://CTChartStyle) + */ + var chartStyle: CTStyle { get set } + /** Customisable `Text` to display when where is not enough data to draw the chart. */ @@ -91,6 +101,15 @@ public protocol ChartData: ObservableObject, Identifiable { */ func getHeaderLocation() -> InfoBoxPlacement + + + + +// func getInfoColor() -> (Color, Color) + + + + /** Gets the nearest data points to the touch location. - Parameters: @@ -196,13 +215,24 @@ public protocol MultiDataSet: DataSet { - Tag: CTChartStyle */ public protocol CTChartStyle { + /** Placement of the information box that appears on touch input. # Reference [See InfoBoxPlacement](x-source-tag://InfoBoxPlacement) */ - var infoBoxPlacement : InfoBoxPlacement { get set } + 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 infoBoxDescriptionColor : Color { get set } /** Global control of animations. diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index a0551499..27c36731 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -11,34 +11,19 @@ internal struct HeaderBox: ViewModifier where T: ChartData { @ObservedObject var chartData: T - let showTitle : Bool - let showSubtitle: Bool - - init(chartData : T, - showTitle : Bool = true, - showSubtitle : Bool = true - ) { - self.chartData = chartData - 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) } } @@ -48,8 +33,10 @@ internal struct HeaderBox: ViewModifier where T: ChartData { ForEach(chartData.infoView.touchOverlayInfo, id: \.self) { info in Text("\(info.value, specifier: chartData.infoView.touchSpecifier)") .font(.title3) + .foregroundColor(chartData.chartStyle.infoBoxValueColour) Text("\(info.pointDescription ?? "")") .font(.subheadline) + .foregroundColor(chartData.chartStyle.infoBoxDescriptionColor) } } else { Text("") diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift index 00ca3d8f..f7b04b95 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift @@ -11,10 +11,12 @@ internal struct Legends: ViewModifier where T: ChartData { @ObservedObject var chartData: T + let textColor: Color + internal func body(content: Content) -> some View { VStack { content - LegendView(chartData: chartData) + LegendView(chartData: chartData, textColor: textColor) } } } @@ -28,8 +30,8 @@ extension View { - Tag: Legends */ - public func legends(chartData: T) -> some View { - self.modifier(Legends(chartData: chartData)) + public func legends(chartData: T, textColor: Color = Color.primary) -> some View { + self.modifier(Legends(chartData: chartData, textColor: textColor)) } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 9a18958b..3900711e 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -19,15 +19,14 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { @ObservedObject var chartData: T /// Decimal precision for labels - private let specifier : String - private let touchMarkerLineWidth : CGFloat = 1 // API? - + private let specifier : String + /// Current location of the touch input @State private var touchLocation : CGPoint = CGPoint(x: 0, y: 0) /// The data point closest to the touch input - @State private var selectedPoints : [T.DataPoint] = [] + @State private var selectedPoints : [T.DataPoint] = [] /// The location for the nearest data point to the touch input - @State private var pointLocations : [HashablePoint] = [HashablePoint(x: 0, y: 0)] + @State private var pointLocations : [HashablePoint] = [HashablePoint(x: 0, y: 0)] /// Frame information of the data point information box @State private var boxFrame : CGRect = CGRect(x: 0, y: 0, width: 0, height: 50) /// Placement of the data point information box @@ -39,11 +38,11 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { /// - Parameters: /// - chartData: /// - specifier: Decimal precision for labels - internal init(chartData: T, - specifier: String + internal init(chartData : T, + specifier : String ) { - self.chartData = chartData - self.specifier = specifier + self.chartData = chartData + self.specifier = specifier } internal func body(content: Content) -> some View { @@ -81,10 +80,14 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { if chartData.infoView.isTouchCurrent { ForEach(pointLocations, id: \.self) { location in TouchOverlayMarker(position: location) - .stroke(Color(.gray), lineWidth: touchMarkerLineWidth) + .stroke(Color(.gray), lineWidth: 1) } if chartData.getHeaderLocation() == .floating { - TouchOverlayBox(selectedPoints: selectedPoints, specifier: specifier, boxFrame: $boxFrame) + TouchOverlayBox(selectedPoints : selectedPoints, + specifier : specifier, + valueColour : chartData.chartStyle.infoBoxValueColour, + descriptionColour: chartData.chartStyle.infoBoxDescriptionColor, + boxFrame : $boxFrame) .position(x: boxLocation.x, y: 0 + (boxFrame.height / 2)) } } @@ -157,7 +160,8 @@ extension View { public func touchOverlay(chartData: T, specifier: String = "%.0f" ) -> some View { - self.modifier(TouchOverlay(chartData: chartData, specifier: specifier)) + self.modifier(TouchOverlay(chartData: chartData, + specifier: specifier)) } #elseif os(tvOS) /** diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift index 1a6036d1..426a9580 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift @@ -35,6 +35,7 @@ internal struct XAxisLabels: ViewModifier where T: LineAndBarChartData { ForEach(labelArray, id: \.self) { data in Text(data) .font(.caption) + .foregroundColor(chartData.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) if data != labelArray[labelArray.count - 1] { @@ -54,6 +55,7 @@ internal struct XAxisLabels: ViewModifier where T: LineAndBarChartData { .frame(minWidth: 0, maxWidth: 500) Text(data) .font(.caption) + .foregroundColor(chartData.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) Spacer() diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift index fcadda25..59e84037 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift @@ -41,6 +41,7 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { ForEach((0...numberOfLabels).reversed(), id: \.self) { i in Text("\(labelsArray[i], specifier: specifier)") .font(.caption) + .foregroundColor(chartData.chartStyle.yAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) if i != 0 { diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift index 6f6559c4..83efe237 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift @@ -17,8 +17,9 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { private let lineColour : Color private let strokeStyle : StrokeStyle - private let labelPosition : DisplayValue - private let labelBackground: Color + private let labelPosition : DisplayValue + private let labelColour : Color + private let labelBackground : Color private let range : Double private let minValue : Double @@ -28,6 +29,7 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { markerName : String, markerValue : Double = 0, labelPosition : DisplayValue, + labelColour : Color, labelBackground: Color, lineColour : Color, strokeStyle : StrokeStyle, @@ -39,6 +41,7 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { self.strokeStyle = strokeStyle self.labelPosition = labelPosition + self.labelColour = labelColour self.labelBackground = labelBackground self.markerValue = isAverage ? chartData.getAverage() : markerValue @@ -90,9 +93,10 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { case .yAxis(specifier: let specifier): Text("\(markerValue, specifier: specifier)") + .font(.caption) + .foregroundColor(labelColour) .padding(4) .background(labelBackground) - .font(.caption) .ifElse(self.chartData.chartStyle.yAxisLabelPosition == .leading, if: { $0.position(x: -18, y: pointY) @@ -100,11 +104,13 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { $0.position(x: geo.size.width + 18, y: pointY) }) + case .center(specifier: let specifier): Text("\(markerValue, specifier: specifier)") .font(.caption) + .foregroundColor(labelColour) .padding() .background(labelBackground) .clipShape(DiamondShape()) @@ -127,6 +133,10 @@ extension View { ``` .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, @@ -153,24 +163,29 @@ extension View { - 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. - Tag: YAxisPOI */ - public func yAxisPOI(chartData : T, - markerName : String, - markerValue : Double, + 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) + 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, @@ -188,6 +203,9 @@ extension View { ``` .averageLine(chartData: data, markerName: "Average", + labelPosition: .center(specifier: "%.0f"), + labelColour: Color.white, + labelBackground: Color.red, lineColour: .primary, strokeStyle: StrokeStyle(lineWidth: 2, lineCap: .round, @@ -213,6 +231,9 @@ extension View { - 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. @@ -222,6 +243,7 @@ extension View { 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) @@ -229,6 +251,7 @@ extension View { self.modifier(YAxisPOI(chartData : chartData, markerName : markerName, labelPosition : labelPosition, + labelColour : labelColour, labelBackground: labelBackground, lineColour : lineColour, strokeStyle : strokeStyle, diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index 4bcfb520..0049e5b5 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -10,9 +10,12 @@ import SwiftUI internal struct LegendView: View where T: ChartData { @ObservedObject var chartData : T + + let textColor: Color - internal init(chartData: T) { + internal init(chartData: T, textColor: Color) { self.chartData = chartData + self.textColor = textColor } // Expose to API ?? @@ -57,6 +60,7 @@ internal struct LegendView: View where T: ChartData { .frame(width: 40, height: 3) Text(legend.legend) .font(.caption) + .foregroundColor(textColor) } } else if let colours = legend.colours { @@ -69,6 +73,7 @@ internal struct LegendView: View where T: ChartData { .frame(width: 40, height: 3) Text(legend.legend) .font(.caption) + .foregroundColor(textColor) } } else if let stops = legend.stops { let stops = GradientStop.convertToGradientStopsArray(stops: stops) @@ -81,6 +86,7 @@ internal struct LegendView: View where T: ChartData { .frame(width: 40, height: 3) Text(legend.legend) .font(.caption) + .foregroundColor(textColor) } } } diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index 29cc4b8e..b000b406 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -11,17 +11,25 @@ internal struct TouchOverlayBox: View { private var selectedPoints : [D] private var specifier : String + + private let valueColour : Color + private let descriptionColour : Color + private var ignoreZero : Bool @Binding private var boxFrame : CGRect internal init(selectedPoints : [D], - specifier : String = "%.0f", + specifier : String = "%.0f", + valueColour : Color, + descriptionColour : Color, boxFrame : Binding, - ignoreZero : Bool = false + ignoreZero : Bool = false ) { - self.selectedPoints = selectedPoints + self.selectedPoints = selectedPoints self.specifier = specifier + self.valueColour = valueColour + self.descriptionColour = descriptionColour self._boxFrame = boxFrame self.ignoreZero = ignoreZero } @@ -31,11 +39,14 @@ internal struct TouchOverlayBox: View { ForEach(selectedPoints, id: \.self) { point in if ignoreZero && point.value != 0 { Text("\(point.value, specifier: specifier)") + .foregroundColor(valueColour) } else if !ignoreZero { Text("\(point.value, specifier: specifier)") + .foregroundColor(valueColour) } if let label = point.pointDescription { Text(label) + .foregroundColor(descriptionColour) } } } From 04ee3aa017b8cdeb91416512a1a23130279f3b94 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Mon, 8 Feb 2021 11:19:15 +0000 Subject: [PATCH 028/152] Fix for tvOS. --- Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 3900711e..bcff57f1 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -170,7 +170,9 @@ extension View { - Attention: Unavailable in tvOS */ - public func touchOverlay(specifier: String = "%.0f") -> some View { + public func touchOverlay(chartData: T, + specifier: String = "%.0f" + ) -> some View { self.modifier(EmptyModifier()) } #endif From 6b188e4bfc15e4227f0db9e3718e8e767af34f61 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Mon, 8 Feb 2021 11:55:02 +0000 Subject: [PATCH 029/152] Update documentation. --- .../BarChart/Models/BarChartData.swift | 22 ++++++---- .../BarChart/Models/MultiBarChartData.swift | 20 +++++---- .../LineChart/Models/LineChartData.swift | 22 ++++++---- .../LineChart/Models/LineChartStyle.swift | 44 ++++++++++--------- .../LineChart/Models/MultiLineChartData.swift | 22 ++++++---- 5 files changed, 75 insertions(+), 55 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index 16b112d1..ad1fecee 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -116,14 +116,18 @@ import SwiftUI ## BarChartStyle ``` - BarChartStyle(infoBoxPlacement : InfoBoxPlacement, - xAxisGridStyle : GridStyle, - yAxisGridStyle : GridStyle, - xAxisLabelPosition : XAxisLabelPosistion, - xAxisLabelsFrom : LabelsFrom, - yAxisLabelPosition : YAxisLabelPosistion, - yAxisNumberOfLabels : Int, - globalAnimation : Animation) + BarChartStyle(infoBoxPlacement : InfoBoxPlacement, + infoBoxValueColour : Color, + infoBoxDescriptionColor : Color, + xAxisGridStyle : GridStyle, + xAxisLabelPosition : XAxisLabelPosistion, + xAxisLabelColour : Color, + xAxisLabelsFrom : LabelsFrom, + yAxisGridStyle : GridStyle, + yAxisLabelPosition : YAxisLabelPosistion, + yAxisLabelColour : Color, + yAxisNumberOfLabels : Int, + globalAnimation : Animation) ``` ### GridStyle @@ -162,6 +166,7 @@ import SwiftUI - Tag: BarChartData */ + public class BarChartData: BarChartDataProtocol { public let id : UUID = UUID() @@ -330,4 +335,3 @@ public class BarChartData: BarChartDataProtocol { public typealias DataPoint = BarChartDataPoint } - diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift index 119b603b..a7ae663f 100644 --- a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift @@ -129,14 +129,18 @@ import SwiftUI ## BarChartStyle ``` - BarChartStyle(infoBoxPlacement : InfoBoxPlacement, - xAxisGridStyle : GridStyle, - yAxisGridStyle : GridStyle, - xAxisLabelPosition : XAxisLabelPosistion, - xAxisLabelsFrom : LabelsFrom, - yAxisLabelPosition : YAxisLabelPosistion, - yAxisNumberOfLabels : Int, - globalAnimation : Animation) + BarChartStyle(infoBoxPlacement : InfoBoxPlacement, + infoBoxValueColour : Color, + infoBoxDescriptionColor : Color, + xAxisGridStyle : GridStyle, + xAxisLabelPosition : XAxisLabelPosistion, + xAxisLabelColour : Color, + xAxisLabelsFrom : LabelsFrom, + yAxisGridStyle : GridStyle, + yAxisLabelPosition : YAxisLabelPosistion, + yAxisLabelColour : Color, + yAxisNumberOfLabels : Int, + globalAnimation : Animation) ``` ### GridStyle diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index d59f5fdf..7a5d6244 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -100,15 +100,19 @@ import SwiftUI ## LineChartStyle ``` - LineChartStyle(infoBoxPlacement : InfoBoxPlacement, - xAxisGridStyle : GridStyle, - yAxisGridStyle : GridStyle, - xAxisLabelPosition : XAxisLabelPosistion, - xAxisLabelsFrom : LabelsFrom, - yAxisLabelPosition : YAxisLabelPosistion, - yAxisNumberOfLabels : Int, - baseline : Baseline, - globalAnimation : Animation) + LineChartStyle(infoBoxPlacement : InfoBoxPlacement, + infoBoxValueColour : Color, + infoBoxDescriptionColor : Color, + xAxisGridStyle : GridStyle, + xAxisLabelPosition : XAxisLabelPosistion, + xAxisLabelColour : Color, + xAxisLabelsFrom : LabelsFrom, + yAxisGridStyle : GridStyle, + yAxisLabelPosition : YAxisLabelPosistion, + yAxisLabelColour : Color, + yAxisNumberOfLabels : Int, + baseline : Baseline, + globalAnimation : Animation) ``` ### GridStyle diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift index 3b74166f..fec45d60 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift @@ -15,19 +15,19 @@ import SwiftUI # Example ``` - LineChartStyle(infoBoxPlacement: .header, - xAxisGridStyle : GridStyle(numberOfLines: 7, - lineColour : .gray, - lineWidth : 1, - dash : [8], - dashPhase : 0), - yAxisGridStyle : GridStyle(numberOfLines: 7, - lineColour : .gray, - lineWidth : 1, - dash : [8], - dashPhase : 0), + LineChartStyle(infoBoxPlacement : .header, + xAxisGridStyle : GridStyle(numberOfLines: 7, + lineColour : .gray, + lineWidth : 1, + dash : [8], + dashPhase : 0), xAxisLabelPosition : .bottom, xAxisLabelsFrom : .dataPoint, + yAxisGridStyle : GridStyle(numberOfLines: 7, + lineColour : .gray, + lineWidth : 1, + dash : [8], + dashPhase : 0), yAxisLabelPosition : .leading, yAxisNumberOfLabels : 5, baseline : .minimumValue, @@ -36,15 +36,19 @@ import SwiftUI # Options ``` - LineChartStyle(infoBoxPlacement : InfoBoxPlacement, - xAxisGridStyle : GridStyle, - yAxisGridStyle : GridStyle, - xAxisLabelPosition : XAxisLabelPosistion, - xAxisLabelsFrom : LabelsFrom, - yAxisLabelPosition : YAxisLabelPosistion, - yAxisNumberOfLabels : Int, - baseline : Baseline, - globalAnimation : Animation) + LineChartStyle(infoBoxPlacement: InfoBoxPlacement, + infoBoxValueColour: Color, + infoBoxDescriptionColor: Color, + xAxisGridStyle: GridStyle, + xAxisLabelPosition: XAxisLabelPosistion, + xAxisLabelColour: Color, + xAxisLabelsFrom: LabelsFrom, + yAxisGridStyle: GridStyle, + yAxisLabelPosition: YAxisLabelPosistion, + yAxisLabelColour: Color, + yAxisNumberOfLabels: Int, + baseline: Baseline, + globalAnimation: Animation) ``` --- diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift index 0129dc67..1f51218c 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -113,15 +113,19 @@ import SwiftUI ## LineChartStyle ``` - LineChartStyle(infoBoxPlacement : InfoBoxPlacement, - xAxisGridStyle : GridStyle, - yAxisGridStyle : GridStyle, - xAxisLabelPosition : XAxisLabelPosistion, - xAxisLabelsFrom : LabelsFrom, - yAxisLabelPosition : YAxisLabelPosistion, - yAxisNumberOfLabels : Int, - baseline : Baseline, - globalAnimation : Animation) + LineChartStyle(infoBoxPlacement : InfoBoxPlacement, + infoBoxValueColour : Color, + infoBoxDescriptionColor : Color, + xAxisGridStyle : GridStyle, + xAxisLabelPosition : XAxisLabelPosistion, + xAxisLabelColour : Color, + xAxisLabelsFrom : LabelsFrom, + yAxisGridStyle : GridStyle, + yAxisLabelPosition : YAxisLabelPosistion, + yAxisLabelColour : Color, + yAxisNumberOfLabels : Int, + baseline : Baseline, + globalAnimation : Animation) ``` ### GridStyle From e53ccf7cde71d2708e4b46bba1aefd60ce200989 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Mon, 8 Feb 2021 12:39:24 +0000 Subject: [PATCH 030/152] Add shape around y axis marker label. --- .../Shared/Shapes/LabelShape.swift | 21 ++++++++++++++ .../Shared/ViewModifiers/YAxisPOI.swift | 29 ++++++++++++------- 2 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 Sources/SwiftUICharts/Shared/Shapes/LabelShape.swift diff --git a/Sources/SwiftUICharts/Shared/Shapes/LabelShape.swift b/Sources/SwiftUICharts/Shared/Shapes/LabelShape.swift new file mode 100644 index 00000000..0c039e97 --- /dev/null +++ b/Sources/SwiftUICharts/Shared/Shapes/LabelShape.swift @@ -0,0 +1,21 @@ +// +// LabelShape.swift +// +// +// Created by Will Dale on 08/02/2021. +// + +import SwiftUI + +public struct LabelShape: 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 + } +} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift index 83efe237..78159761 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift @@ -12,6 +12,8 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData: T + private let uuid = UUID() + private let markerName : String private var markerValue : Double private let lineColour : Color @@ -58,6 +60,16 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { valueLabel // } } + .onAppear { + if !chartData.legends.contains(where: { $0.id == uuid }) { // init twice + chartData.legends.append(LegendData(id : uuid, + legend : markerName, + colour : lineColour, + strokeStyle : Stroke.strokeStyleToStroke(strokeStyle: strokeStyle), + prioity : 2, + chartType : .line)) + } + } } var marker: some View { @@ -67,16 +79,7 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { maxValue : maxValue, chartType : chartData.chartType.chartType) .stroke(lineColour, style: strokeStyle) - .onAppear { - if !chartData.legends.contains(where: { $0.legend == markerName }) { // init twice - chartData.legends.append(LegendData(id : UUID(), - legend : markerName, - colour : lineColour, - strokeStyle : Stroke.strokeStyleToStroke(strokeStyle: strokeStyle), - prioity : 2, - chartType : .line)) - } - } + } var valueLabel: some View { @@ -96,7 +99,11 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { .font(.caption) .foregroundColor(labelColour) .padding(4) - .background(labelBackground) + .background(Color.blue) + .clipShape(LabelShape()) + .overlay(LabelShape() + .stroke(lineColour) + ) .ifElse(self.chartData.chartStyle.yAxisLabelPosition == .leading, if: { $0.position(x: -18, y: pointY) From 4b836b3d33269b7148859a0afcf86698974282b1 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Mon, 8 Feb 2021 17:19:02 +0000 Subject: [PATCH 031/152] General tidy up. --- .../BarChart/Models/BarChartData.swift | 46 ++- .../BarChart/Models/BarChartEnums.swift | 22 ++ .../BarChart/Models/MultiBarChartData.swift | 47 ++- .../LineChart/Models/LineChartData.swift | 51 +++- .../LineChart/Models/LineChartEnums.swift | 81 +++++ .../LineChart/Models/LineChartProtocols.swift | 2 + .../LineChart/Models/MultiLineChartData.swift | 49 ++- .../Models/PointStyle.swift | 0 .../Shapes/LegendLine.swift | 0 .../Shapes/PointShape.swift | 0 .../SwiftUICharts/Shared/Extras/Enums.swift | 284 ------------------ .../Shared/Extras/Extensions.swift | 1 - .../Shared/Extras/SharedEnums.swift | 129 ++++++++ .../Shared/Models/SharedProtocols.swift | 9 - .../Shared/ViewModifiers/Legends.swift | 17 +- .../Shared/ViewModifiers/XAxisLabels.swift | 118 -------- .../Shared/Views/CustomNoDataView.swift | 24 +- .../Shared/Views/LegendView.swift | 16 +- .../Models/ChartViewData.swift | 0 .../Models/GridStyle.swift | 0 .../Models/LineAndBarEnums.swift | 75 +++++ .../Models}/LineAndBarProtocols.swift | 73 +++-- .../Shapes/DiamondShape.swift | 6 - .../Shapes/HorizontalGridShape.swift | 17 ++ .../Shapes/LabelShape.swift | 0 .../Shapes/Marker.swift | 0 .../Shapes/VerticalGridShape.swift | 17 ++ .../ViewModifiers/AxisBorders.swift | 34 ++- .../ViewModifiers/XAxisGrid.swift | 37 --- .../ViewModifiers/XAxisLabels.swift | 64 ++++ .../ViewModifiers/YAxisGrid.swift | 38 --- .../ViewModifiers/YAxisLabels.swift | 0 .../ViewModifiers/YAxisPOI.swift | 0 .../Views/HorizontalGridView.swift | 31 ++ .../Views/VerticalGridView.swift | 31 ++ 35 files changed, 705 insertions(+), 614 deletions(-) create mode 100644 Sources/SwiftUICharts/BarChart/Models/BarChartEnums.swift create mode 100644 Sources/SwiftUICharts/LineChart/Models/LineChartEnums.swift rename Sources/SwiftUICharts/{Shared => LineChart}/Models/PointStyle.swift (100%) rename Sources/SwiftUICharts/{Shared => LineChart}/Shapes/LegendLine.swift (100%) rename Sources/SwiftUICharts/{Shared => LineChart}/Shapes/PointShape.swift (100%) delete mode 100644 Sources/SwiftUICharts/Shared/Extras/Enums.swift create mode 100644 Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift delete mode 100644 Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift rename Sources/SwiftUICharts/{Shared => SharedLineAndBar}/Models/ChartViewData.swift (100%) rename Sources/SwiftUICharts/{Shared => SharedLineAndBar}/Models/GridStyle.swift (100%) create mode 100644 Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarEnums.swift rename Sources/SwiftUICharts/{Shared/Models/LineAndBar => SharedLineAndBar/Models}/LineAndBarProtocols.swift (98%) rename Sources/SwiftUICharts/{Shared => SharedLineAndBar}/Shapes/DiamondShape.swift (89%) create mode 100644 Sources/SwiftUICharts/SharedLineAndBar/Shapes/HorizontalGridShape.swift rename Sources/SwiftUICharts/{Shared => SharedLineAndBar}/Shapes/LabelShape.swift (100%) rename Sources/SwiftUICharts/{Shared => SharedLineAndBar}/Shapes/Marker.swift (100%) create mode 100644 Sources/SwiftUICharts/SharedLineAndBar/Shapes/VerticalGridShape.swift rename Sources/SwiftUICharts/{Shared => SharedLineAndBar}/ViewModifiers/AxisBorders.swift (64%) rename Sources/SwiftUICharts/{Shared => SharedLineAndBar}/ViewModifiers/XAxisGrid.swift (56%) create mode 100644 Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift rename Sources/SwiftUICharts/{Shared => SharedLineAndBar}/ViewModifiers/YAxisGrid.swift (55%) rename Sources/SwiftUICharts/{Shared => SharedLineAndBar}/ViewModifiers/YAxisLabels.swift (100%) rename Sources/SwiftUICharts/{Shared => SharedLineAndBar}/ViewModifiers/YAxisPOI.swift (100%) create mode 100644 Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift create mode 100644 Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index ad1fecee..11cbc372 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -238,17 +238,41 @@ public class BarChartData: BarChartDataProtocol { return locations } - public func getXAxidLabels() -> some View { - HStack(spacing: 0) { - ForEach(dataSets.dataPoints) { data in - Spacer() - .frame(minWidth: 0, maxWidth: 500) - Text(data.xAxisLabel ?? "") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) - Spacer() - .frame(minWidth: 0, maxWidth: 500) + public func getXAxisLabels() -> some View { + Group { + switch self.chartStyle.xAxisLabelsFrom { + case .dataPoint: + + HStack(spacing: 0) { + ForEach(dataSets.dataPoints) { data in + Spacer() + .frame(minWidth: 0, maxWidth: 500) + Text(data.xAxisLabel ?? "") + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + 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) + Text(data) + .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) + .lineLimit(1) + .minimumScaleFactor(0.5) + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } } } } diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartEnums.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartEnums.swift new file mode 100644 index 00000000..631cd72b --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartEnums.swift @@ -0,0 +1,22 @@ +// +// 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 + ``` + + - Tag: ColourFrom + */ +public enum ColourFrom { + case barStyle + case dataPoints +} diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift index a7ae663f..6603da3d 100644 --- a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift @@ -252,16 +252,42 @@ public class MultiBarChartData: BarChartDataProtocol { } return locations } - public func getXAxidLabels() -> some View { - HStack(spacing: 100) { - ForEach(dataSets.dataSets) { dataSet in - HStack(spacing: 0) { - ForEach(dataSet.dataPoints) { data in - Text(data.xAxisLabel ?? "") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) - if data != dataSet.dataPoints[dataSet.dataPoints.count - 1] { + + public func getXAxisLabels() -> some View { + Group { + switch self.chartStyle.xAxisLabelsFrom { + case .dataPoint: + + HStack(spacing: 100) { + ForEach(dataSets.dataSets) { dataSet in + HStack(spacing: 0) { + ForEach(dataSet.dataPoints) { data in + Text(data.xAxisLabel ?? "") + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + if data != dataSet.dataPoints[dataSet.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 + Spacer() + .frame(minWidth: 0, maxWidth: 500) + Text(data) + .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) + .lineLimit(1) + .minimumScaleFactor(0.5) Spacer() .frame(minWidth: 0, maxWidth: 500) } @@ -269,7 +295,6 @@ public class MultiBarChartData: BarChartDataProtocol { } } } - .padding(.horizontal, -4) } public func setupLegends() { diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index 7a5d6244..19808b40 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -228,23 +228,47 @@ public class LineChartData: LineChartDataProtocol { } // MARK: Labels - // TODO --- Add from xaxis labels - public func getXAxidLabels() -> some View { - HStack(spacing: 0) { - ForEach(dataSets.dataPoints) { data in - if let label = data.xAxisLabel { - Text(label) - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) + public func getXAxisLabels() -> some View { + Group { + switch self.chartStyle.xAxisLabelsFrom { + case .dataPoint: + + HStack(spacing: 0) { + ForEach(dataSets.dataPoints) { data in + if let label = data.xAxisLabel { + Text(label) + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + } + if data != self.dataSets.dataPoints[self.dataSets.dataPoints.count - 1] { + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } } - 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 + Text(data) + .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) + .lineLimit(1) + .minimumScaleFactor(0.5) + if data != labelArray[labelArray.count - 1] { + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } + .padding(.horizontal, -4) } } } - .padding(.horizontal, -4) } // MARK: Touch @@ -312,6 +336,7 @@ public class LineChartData: LineChartDataProtocol { chartType : .line)) } } + public typealias Set = LineDataSet public typealias DataPoint = LineChartDataPoint } diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartEnums.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartEnums.swift new file mode 100644 index 00000000..934e24d6 --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartEnums.swift @@ -0,0 +1,81 @@ +// +// 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 + ``` + + - Tag: LineType + */ +public enum LineType { + /// Straight line from point to point + case line + /// Dual control point curved line + case curvedLine +} + +/** + 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 + ``` + + - Tag: Baseline + */ +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 +} + +/** + Style of the point marks + ``` + case filled // Just fill + case outline // Just stroke + case filledOutLine // Both fill and stroke + ``` + + - Tag: PointType + */ +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 + ``` + + - Tag: PointShape + */ +public enum PointShape { + /// Circle Shape + case circle + /// Square Shape + case square + /// Rounded Square Shape + case roundSquare +} diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift index 4cc4cde9..3ccfa1fe 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift @@ -36,6 +36,8 @@ extension LineAndBarChartData where Self: LineChartDataProtocol { return labels } } + + extension LineAndBarChartData where Self: LineChartData { public func getRange() -> Double { switch self.chartStyle.baseline { diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift index 1f51218c..1795ee58 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -257,22 +257,47 @@ public class MultiLineChartData: LineChartDataProtocol { } return locations } - public func getXAxidLabels() -> some View { - HStack(spacing: 0) { - ForEach(dataSets.dataSets[0].dataPoints) { data in - if let label = data.xAxisLabel { - Text(label) - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) + public func getXAxisLabels() -> some View { + Group { + switch self.chartStyle.xAxisLabelsFrom { + case .dataPoint: + + HStack(spacing: 0) { + ForEach(dataSets.dataSets[0].dataPoints) { data in + if let label = data.xAxisLabel { + Text(label) + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + } + if data != self.dataSets.dataSets[0].dataPoints[self.dataSets.dataSets[0].dataPoints.count - 1] { + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } } - 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 + Text(data) + .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) + .lineLimit(1) + .minimumScaleFactor(0.5) + if data != labelArray[labelArray.count - 1] { + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } + .padding(.horizontal, -4) } } } - .padding(.horizontal, -4) } public func setupLegends() { diff --git a/Sources/SwiftUICharts/Shared/Models/PointStyle.swift b/Sources/SwiftUICharts/LineChart/Models/PointStyle.swift similarity index 100% rename from Sources/SwiftUICharts/Shared/Models/PointStyle.swift rename to Sources/SwiftUICharts/LineChart/Models/PointStyle.swift diff --git a/Sources/SwiftUICharts/Shared/Shapes/LegendLine.swift b/Sources/SwiftUICharts/LineChart/Shapes/LegendLine.swift similarity index 100% rename from Sources/SwiftUICharts/Shared/Shapes/LegendLine.swift rename to Sources/SwiftUICharts/LineChart/Shapes/LegendLine.swift diff --git a/Sources/SwiftUICharts/Shared/Shapes/PointShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift similarity index 100% rename from Sources/SwiftUICharts/Shared/Shapes/PointShape.swift rename to Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift diff --git a/Sources/SwiftUICharts/Shared/Extras/Enums.swift b/Sources/SwiftUICharts/Shared/Extras/Enums.swift deleted file mode 100644 index 6d04e20f..00000000 --- a/Sources/SwiftUICharts/Shared/Extras/Enums.swift +++ /dev/null @@ -1,284 +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 - ``` - - - Tag: CalculationType - */ -public enum CalculationType { - /// No function - case none - /// Monthly Average - case averageMonth - /// Weekly Average - case averageWeek - /// Daily Average - case averageDay -} - -// MARK: - ChartViewData -/** - The type of `DataSet` being used - ``` - case single // Single data set - i.e LineDataSet - case multi // Multi data set - i.e MultiLineDataSet - ``` - - - Tag: DataSetType - */ -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 - ``` - - - Tag: ChartType - */ -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 - ``` - - - Tag: ColourType - */ -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 - ``` - - - Tag: LineType - */ -public enum LineType { - /// Straight line from point to point - case line - /// Dual control point curved line - case curvedLine -} - -/** - 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 - ``` - - - Tag: Baseline - */ -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 -} - -// MARK: - BarStyle -/** - Where to get the colour data from. - ``` - case barStyle // From BarStyle data model - case dataPoints // From each data point - ``` - - - Tag: ColourFrom - */ -public enum ColourFrom { - case barStyle - case dataPoints -} - -// 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() - ``` - - - Tag: InfoBoxPlacement - */ -public enum InfoBoxPlacement { - /// Follows input across the chart - case floating - /// Fix in the Header box. Must have .headerBox() - case header -} - -/** - 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 - ``` - - - Tag: MarkerType - */ -public enum MarkerType { - /// 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 - ``` - - - Tag: PointType - */ -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 - ``` - - - Tag: PointShape - */ -public enum PointShape { - /// Circle Shape - case circle - /// Square Shape - case square - /// Rounded Square Shape - case roundSquare -} - -// MARK: - XAxisLabels -/** -Location of the X axis labels - ``` - case top - case bottom - ``` - - - Tag: XAxisLabelPosistion - */ -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 - ``` - - - Tag: LabelsFrom - */ -public enum LabelsFrom { - /// ChartData --> DataPoint --> xAxisLabel - case dataPoint - /// ChartData --> xAxisLabels - case chartData -} - -// MARK: - YAxisLabels -/** -Location of the Y axis labels - ``` - case leading - case trailing - ``` - - - Tag: YAxisLabelPosistion - */ -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) -} diff --git a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift index f6deb6b7..e6495192 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift @@ -22,7 +22,6 @@ extension View { if ifTransform: (Self) -> TrueContent, else elseTransform: (Self) -> FalseContent ) -> some View { - if condition { ifTransform(self) } else { diff --git a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift new file mode 100644 index 00000000..b930cd54 --- /dev/null +++ b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift @@ -0,0 +1,129 @@ +// +// 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 + ``` + + - Tag: CalculationType + */ +public enum CalculationType { + /// No function + case none + /// Monthly Average + case averageMonth + /// Weekly Average + case averageWeek + /// Daily Average + case averageDay +} + +// MARK: - ChartViewData +/** + The type of `DataSet` being used + ``` + case single // Single data set - i.e LineDataSet + case multi // Multi data set - i.e MultiLineDataSet + ``` + + - Tag: DataSetType + */ +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 + ``` + + - Tag: ChartType + */ +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 + ``` + + - Tag: ColourType + */ +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 header // Fix in the Header box. Must have .headerBox() + ``` + + - Tag: InfoBoxPlacement + */ +public enum InfoBoxPlacement { + /// Follows input across the chart + case floating + /// Fix in the Header box. Must have .headerBox() + case header +} + + +/** + 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 + ``` + + - Tag: MarkerType + */ +public enum MarkerType { + /// 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 +} diff --git a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift index 444d2446..63675a0b 100644 --- a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift @@ -101,15 +101,6 @@ public protocol ChartData: ObservableObject, Identifiable { */ func getHeaderLocation() -> InfoBoxPlacement - - - - -// func getInfoColor() -> (Color, Color) - - - - /** Gets the nearest data points to the touch location. - Parameters: diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift index f7b04b95..c95ded4f 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift @@ -10,13 +10,22 @@ import SwiftUI internal struct Legends: ViewModifier where T: ChartData { @ObservedObject var chartData: T + private let columns : [GridItem] + private let textColor : Color - 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, textColor: textColor) + LegendView(chartData: chartData, columns: columns, textColor: textColor) } } } @@ -30,8 +39,8 @@ extension View { - Tag: Legends */ - public func legends(chartData: T, textColor: Color = Color.primary) -> some View { - self.modifier(Legends(chartData: chartData, textColor: textColor)) + 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/XAxisLabels.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift deleted file mode 100644 index 426a9580..00000000 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisLabels.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// XAxisLabels.swift -// LineChart -// -// Created by Will Dale on 26/12/2020. -// - -import SwiftUI - -internal struct XAxisLabels: ViewModifier where T: LineAndBarChartData { - - @ObservedObject var chartData: T - - internal init(chartData: T) { - self.chartData = chartData - - self.chartData.viewData.hasXAxisLabels = true - } - - @ViewBuilder - internal var labels: some View { - - switch chartData.chartStyle.xAxisLabelsFrom { - case .dataPoint: - // ChartData -> DataPoints -> xAxisLabel - - chartData.getXAxidLabels() - - case .chartData: - switch chartData.chartType.chartType { - case .line: - // ChartData -> xAxisLabels - if let labelArray = chartData.xAxisLabels { - HStack(spacing: 0) { - ForEach(labelArray, id: \.self) { data in - Text(data) - .font(.caption) - .foregroundColor(chartData.chartStyle.xAxisLabelColour) - .lineLimit(1) - .minimumScaleFactor(0.5) - if data != labelArray[labelArray.count - 1] { - Spacer() - .frame(minWidth: 0, maxWidth: 500) - } - } - } - .padding(.horizontal, -4) - - } - 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) - .foregroundColor(chartData.chartStyle.xAxisLabelColour) - .lineLimit(1) - .minimumScaleFactor(0.5) - Spacer() - .frame(minWidth: 0, maxWidth: 500) - } - } - } - case .pie: - Text("Should not be here") - } - } - } - - @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. - - The labels can either come from ChartData --> xAxisLabels - or ChartData --> DataSets --> DataPoints - - - Requires: - Chart Data to conform to LineAndBarChartData. - - # Available for: - - Line Chart - - Multi Line Chart - - Bar Chart - - Grouped 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. - - - Tag: XAxisLabels - */ - public func xAxisLabels(chartData: T) -> some View { - self.modifier(XAxisLabels(chartData: chartData)) - } -} diff --git a/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift b/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift index 56047e1c..31b47da3 100644 --- a/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift +++ b/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift @@ -7,15 +7,15 @@ import SwiftUI -//public struct CustomNoDataView: View { -// -// let chartData : ChartData -// -// init(chartData: ChartData) { -// self.chartData = chartData -// } -// -// public var body: some View { -// chartData.noDataText -// } -//} +public struct CustomNoDataView: View where T: ChartData { + + let chartData : T + + init(chartData: T) { + self.chartData = chartData + } + + public var body: some View { + chartData.noDataText + } +} diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index 0049e5b5..678c2048 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -11,20 +11,18 @@ internal struct LegendView: View where T: ChartData { @ObservedObject var chartData : T - let textColor: Color + private let columns : [GridItem] + private let textColor : Color - internal init(chartData: T, 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) { diff --git a/Sources/SwiftUICharts/Shared/Models/ChartViewData.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift similarity index 100% rename from Sources/SwiftUICharts/Shared/Models/ChartViewData.swift rename to Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift diff --git a/Sources/SwiftUICharts/Shared/Models/GridStyle.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/GridStyle.swift similarity index 100% rename from Sources/SwiftUICharts/Shared/Models/GridStyle.swift rename to Sources/SwiftUICharts/SharedLineAndBar/Models/GridStyle.swift diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarEnums.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarEnums.swift new file mode 100644 index 00000000..f7df2bd3 --- /dev/null +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarEnums.swift @@ -0,0 +1,75 @@ +// +// LineAndBarEnums.swift +// +// +// Created by Will Dale on 08/02/2021. +// + +import Foundation + +// MARK: - XAxisLabels +/** +Location of the X axis labels + ``` + case top + case bottom + ``` + + - Tag: XAxisLabelPosistion + */ +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 + ``` + + - Tag: LabelsFrom + */ +public enum LabelsFrom { + /// ChartData --> DataPoint --> xAxisLabel + case dataPoint + /// ChartData --> xAxisLabels + case chartData +} + +// MARK: - YAxisLabels +/** +Location of the Y axis labels + ``` + case leading + case trailing + ``` + + - Tag: YAxisLabelPosistion + */ +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) +} diff --git a/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarProtocols.swift similarity index 98% rename from Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift rename to Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarProtocols.swift index ba99065e..29bad9ef 100644 --- a/Sources/SwiftUICharts/Shared/Models/LineAndBar/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarProtocols.swift @@ -48,7 +48,7 @@ public protocol LineAndBarChartData : ChartData where CTStyle: CTLineAndBarChart - Tag: getXAxidLabels */ - func getXAxidLabels() -> Body + func getXAxisLabels() -> Body /** Labels to display on the Y axis @@ -87,41 +87,6 @@ public protocol LineAndBarChartData : ChartData where CTStyle: CTLineAndBarChart func getAverage() -> Double } -extension LineAndBarChartData { - public func getHeaderLocation() -> InfoBoxPlacement { - return self.chartStyle.infoBoxPlacement - } -} - -extension LineAndBarChartData where Set: SingleDataSet { - public func getRange() -> Double { - DataFunctions.dataSetRange(from: dataSets) - } - public func getMinValue() -> Double { - DataFunctions.dataSetMinValue(from: dataSets) - } - public func getMaxValue() -> Double { - DataFunctions.dataSetMaxValue(from: dataSets) - } - public func getAverage() -> Double { - DataFunctions.dataSetAverage(from: dataSets) - } -} -extension LineAndBarChartData where Set: MultiDataSet { - public func getRange() -> Double { - DataFunctions.multiDataSetRange(from: dataSets) - } - public func getMinValue() -> Double { - DataFunctions.multiDataSetMinValue(from: dataSets) - } - public func getMaxValue() -> Double { - DataFunctions.multiDataSetMaxValue(from: dataSets) - } - public func getAverage() -> Double { - DataFunctions.multiDataSetAverage(from: dataSets) - } -} - // MARK: - Style /** @@ -185,7 +150,7 @@ public protocol CTLineAndBarChartStyle: CTChartStyle { } -// MARK: DataPoints +// MARK: - DataPoints /** A protocol to extend functionality of `CTChartDataPoint` specifically for Line and Bar Charts. @@ -200,3 +165,37 @@ public protocol CTLineAndBarDataPoint: CTChartDataPoint { } +// MARK: Extensions +extension LineAndBarChartData { + public func getHeaderLocation() -> InfoBoxPlacement { + return self.chartStyle.infoBoxPlacement + } +} +extension LineAndBarChartData where Set: SingleDataSet { + public func getRange() -> Double { + DataFunctions.dataSetRange(from: dataSets) + } + public func getMinValue() -> Double { + DataFunctions.dataSetMinValue(from: dataSets) + } + public func getMaxValue() -> Double { + DataFunctions.dataSetMaxValue(from: dataSets) + } + public func getAverage() -> Double { + DataFunctions.dataSetAverage(from: dataSets) + } +} +extension LineAndBarChartData where Set: MultiDataSet { + public func getRange() -> Double { + DataFunctions.multiDataSetRange(from: dataSets) + } + public func getMinValue() -> Double { + DataFunctions.multiDataSetMinValue(from: dataSets) + } + public func getMaxValue() -> Double { + DataFunctions.multiDataSetMaxValue(from: dataSets) + } + public func getAverage() -> Double { + DataFunctions.multiDataSetAverage(from: dataSets) + } +} diff --git a/Sources/SwiftUICharts/Shared/Shapes/DiamondShape.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/DiamondShape.swift similarity index 89% rename from Sources/SwiftUICharts/Shared/Shapes/DiamondShape.swift rename to Sources/SwiftUICharts/SharedLineAndBar/Shapes/DiamondShape.swift index 43dbce8b..7b8bb7ac 100644 --- a/Sources/SwiftUICharts/Shared/Shapes/DiamondShape.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/DiamondShape.swift @@ -8,19 +8,13 @@ import SwiftUI 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..d1716bb1 --- /dev/null +++ b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/HorizontalGridShape.swift @@ -0,0 +1,17 @@ +// +// HorizontalGridShape.swift +// +// +// Created by Will Dale on 08/02/2021. +// + +import SwiftUI + +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/Shapes/LabelShape.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/LabelShape.swift similarity index 100% rename from Sources/SwiftUICharts/Shared/Shapes/LabelShape.swift rename to Sources/SwiftUICharts/SharedLineAndBar/Shapes/LabelShape.swift diff --git a/Sources/SwiftUICharts/Shared/Shapes/Marker.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift similarity index 100% rename from Sources/SwiftUICharts/Shared/Shapes/Marker.swift rename to Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/VerticalGridShape.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/VerticalGridShape.swift new file mode 100644 index 00000000..016c0fc1 --- /dev/null +++ b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/VerticalGridShape.swift @@ -0,0 +1,17 @@ +// +// VerticalGridShape.swift +// +// +// Created by Will Dale on 08/02/2021. +// + +import SwiftUI + +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/AxisBorders.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift similarity index 64% rename from Sources/SwiftUICharts/Shared/ViewModifiers/AxisBorders.swift rename to Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift index 20a4f5de..64659c06 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/AxisBorders.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift @@ -10,22 +10,27 @@ import SwiftUI internal struct XAxisBorder: ViewModifier where T: LineAndBarChartData { @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 + } + @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 { + VStack { ZStack(alignment: .bottom) { content Divider() } } } else if labelsAndTop { - VStack { + VStack { ZStack(alignment: .top) { content Divider() @@ -40,15 +45,20 @@ internal struct XAxisBorder: ViewModifier where T: LineAndBarChartData { internal struct YAxisBorder: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData: T + private let labelsAndLeading : Bool + private let labelsAndTrailing: Bool + + init(chartData: T) { + self.chartData = chartData + self.labelsAndLeading = chartData.viewData.hasYAxisLabels && chartData.chartStyle.yAxisLabelPosition == .leading + self.labelsAndTrailing = chartData.viewData.hasYAxisLabels && chartData.chartStyle.yAxisLabelPosition == .trailing + } @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 { + HStack { ZStack(alignment: .leading) { content Divider() @@ -62,7 +72,7 @@ internal struct YAxisBorder: ViewModifier where T: LineAndBarChartData { } } } else { - content + content } } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift similarity index 56% rename from Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift rename to Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift index 7aab03bb..f0e4e799 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/XAxisGrid.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift @@ -58,40 +58,3 @@ extension View { self.modifier(XAxisGrid(chartData: chartData)) } } - - -internal struct VerticalGridView: View where T: LineAndBarChartData { - - var chartData : T - - @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/SharedLineAndBar/ViewModifiers/XAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift new file mode 100644 index 00000000..4195ccb6 --- /dev/null +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift @@ -0,0 +1,64 @@ +// +// XAxisLabels.swift +// LineChart +// +// Created by Will Dale on 26/12/2020. +// + +import SwiftUI + +internal struct XAxisLabels: ViewModifier where T: LineAndBarChartData { + + @ObservedObject var chartData: T + + internal init(chartData: T) { + self.chartData = chartData + self.chartData.viewData.hasXAxisLabels = true + } + + @ViewBuilder + internal func body(content: Content) -> some View { + switch chartData.chartStyle.xAxisLabelPosition { + case .top: + VStack { + chartData.getXAxisLabels() + content + } + case .bottom: + VStack { + content + chartData.getXAxisLabels() + } + } + } +} + +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 LineAndBarChartData. + + # Available for: + - Line Chart + - Multi Line Chart + - Bar Chart + - Grouped 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. + + - Tag: XAxisLabels + */ + public func xAxisLabels(chartData: T) -> some View { + self.modifier(XAxisLabels(chartData: chartData)) + } +} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift similarity index 55% rename from Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift rename to Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift index 801249ba..60a0fad6 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisGrid.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift @@ -17,9 +17,7 @@ internal struct YAxisGrid: ViewModifier where T: LineAndBarChartData { VStack { ForEach((0...chartData.chartStyle.yAxisGridStyle.numberOfLines), id: \.self) { index in if index != 0 { - HorizontalGridView(chartData: chartData) - Spacer() .frame(minHeight: 0, maxHeight: 500) } @@ -60,39 +58,3 @@ extension View { self.modifier(YAxisGrid(chartData: chartData)) } } - - -internal struct HorizontalGridView: View where T: LineAndBarChartData { - - var chartData : T - - @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/SharedLineAndBar/ViewModifiers/YAxisLabels.swift similarity index 100% rename from Sources/SwiftUICharts/Shared/ViewModifiers/YAxisLabels.swift rename to Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift similarity index 100% rename from Sources/SwiftUICharts/Shared/ViewModifiers/YAxisPOI.swift rename to Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift new file mode 100644 index 00000000..2581574d --- /dev/null +++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift @@ -0,0 +1,31 @@ +// +// HorizontalGridView.swift +// +// +// Created by Will Dale on 08/02/2021. +// + +import SwiftUI + +internal struct HorizontalGridView: View where T: LineAndBarChartData { + + var chartData : T + + @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 + } + } +} diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift new file mode 100644 index 00000000..5be1ae5f --- /dev/null +++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift @@ -0,0 +1,31 @@ +// +// VerticalGridView.swift +// +// +// Created by Will Dale on 08/02/2021. +// + +import SwiftUI + +internal struct VerticalGridView: View where T: LineAndBarChartData { + + var chartData : T + + @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 + } + } +} From b76f6e01c2c3e5d51630b7c1087eba03b89de5c2 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Tue, 9 Feb 2021 11:00:33 +0000 Subject: [PATCH 032/152] Add No Data Text to Line and Bar charts. --- .../BarChart/Models/BarChartData.swift | 99 +++++++++----- .../BarChart/Models/MultiBarChartData.swift | 107 ++++++++++----- .../BarChart/Views/BarChartView.swift | 45 ++++--- .../BarChart/Views/GroupedBarChart.swift | 55 ++++---- .../LineChart/Models/LineChartData.swift | 35 +++-- .../LineChart/Models/MultiLineChartData.swift | 125 +++++++++++------- .../ViewModifiers/PointMarkers.swift | 32 +++-- .../LineChart/Views/FilledLineChart.swift | 103 +++++++-------- .../LineChart/Views/LineChartView.swift | 94 ++++++------- .../LineChart/Views/MultiLineChart.swift | 102 +++++++------- .../Shared/Models/SharedProtocols.swift | 25 +++- .../Shared/ViewModifiers/HeaderBox.swift | 52 ++++---- .../Shared/ViewModifiers/Legends.swift | 10 +- .../Shared/ViewModifiers/TouchOverlay.swift | 86 ++++++------ .../Models/LineAndBarProtocols.swift | 2 +- .../ViewModifiers/AxisBorders.swift | 64 ++++----- .../ViewModifiers/XAxisGrid.swift | 7 +- .../ViewModifiers/XAxisLabels.swift | 27 ++-- .../ViewModifiers/YAxisGrid.swift | 8 +- .../ViewModifiers/YAxisLabels.swift | 46 ++++--- .../ViewModifiers/YAxisPOI.swift | 11 +- 21 files changed, 648 insertions(+), 487 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index 11cbc372..d81b5684 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -43,10 +43,11 @@ import SwiftUI yAxisLabelPosition: .leading, yAxisNumberOfLabels: 5) - return BarChartData(dataSets: data, - metadata: metadata, - xAxisLabels: labels, - chartStyle: chartStyle, + return BarChartData(dataSets : data, + metadata : metadata, + xAxisLabels : labels, + chartStyle : chartStyle, + noDataText : Text("No Data"), calculations: .none) } @@ -168,7 +169,7 @@ import SwiftUI */ public class BarChartData: BarChartDataProtocol { - + // MARK: - Properties public let id : UUID = UUID() @Published public var dataSets : BarDataSet @@ -179,65 +180,75 @@ public class BarChartData: BarChartDataProtocol { @Published public var viewData : ChartViewData @Published public var infoView : InfoViewData = InfoViewData() - public var noDataText : Text = Text("No Data") + public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) - + + // MARK: - Initializers + /// Initialises a standard Bar Chart with optional calculation + /// + /// Has the option perform optional calculation on the data set, such as averaging based on date. + /// + /// - Note: + /// To add custom calculations use the initialiser with `customCalc`. + /// + /// - 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. + /// - 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. + /// - calculations: Addition calculations that can be performed on the data set before drawing. public init(dataSets : BarDataSet, metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : BarChartStyle = BarChartStyle(), + noDataText : Text = Text("No Data"), calculations: CalculationType = .none ) { self.dataSets = dataSets self.metadata = metadata self.xAxisLabels = xAxisLabels self.chartStyle = chartStyle + self.noDataText = noDataText self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (.bar, .single) self.setupLegends() } + /// Initializes a standar Bar Chart with custom calculation + /// + /// Has the option perform custom calculations on the data set. + /// + /// - Note: + /// To add pre built calculations use the initialiser with `calculations`. + /// + /// - 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. + /// - 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. + /// - customCalc: Custom calculations that can be performed on the data set before drawing. public init(dataSets : BarDataSet, metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : BarChartStyle = BarChartStyle(), + noDataText : Text = Text("No Data"), customCalc : @escaping ([BarChartDataPoint]) -> [BarChartDataPoint]? ) { self.dataSets = dataSets self.metadata = metadata self.xAxisLabels = xAxisLabels self.chartStyle = chartStyle + self.noDataText = noDataText self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (chartType: .bar, dataSetType: .single) self.setupLegends() } - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { - var points : [BarChartDataPoint] = [] - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count) - let index : Int = Int((touchLocation.x) / xSection) - if index >= 0 && index < dataSets.dataPoints.count { - points.append(dataSets.dataPoints[index]) - } - return points - } - - public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - var locations : [HashablePoint] = [] - - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count) - let ySection : CGFloat = chartSize.size.height / CGFloat(self.getMaxValue()) - let index : Int = Int((touchLocation.x) / xSection) - - if index >= 0 && index < dataSets.dataPoints.count { - locations.append(HashablePoint(x: (CGFloat(index) * xSection) + (xSection / 2), - y: (chartSize.size.height - CGFloat(dataSets.dataPoints[index].value) * ySection))) - } - return locations - } - + // MARK: - Labels public func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { @@ -276,7 +287,33 @@ public class BarChartData: BarChartDataProtocol { } } } + + // MARK: - Touch + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { + var points : [BarChartDataPoint] = [] + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count) + let index : Int = Int((touchLocation.x) / xSection) + if index >= 0 && index < dataSets.dataPoints.count { + points.append(dataSets.dataPoints[index]) + } + return points + } + public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { + var locations : [HashablePoint] = [] + + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count) + let ySection : CGFloat = chartSize.size.height / CGFloat(self.getMaxValue()) + let index : Int = Int((touchLocation.x) / xSection) + + if index >= 0 && index < dataSets.dataPoints.count { + locations.append(HashablePoint(x: (CGFloat(index) * xSection) + (xSection / 2), + y: (chartSize.size.height - CGFloat(dataSets.dataPoints[index].value) * ySection))) + } + return locations + } + + // MARK: - Legends public func setupLegends() { switch self.dataSets.style.colourFrom { case .barStyle: diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift index 6603da3d..d76aa97c 100644 --- a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift @@ -51,10 +51,11 @@ import SwiftUI style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)) ]) - return MultiBarChartData(dataSets: data, - metadata: ChartMetadata(title: "Hello", subtitle: "Bob"), - xAxisLabels: ["Hello"], - chartStyle: BarChartStyle(), + return MultiBarChartData(dataSets : data, + metadata : ChartMetadata(title: "Hello", subtitle: "Bob"), + xAxisLabels : ["Hello"], + chartStyle : BarChartStyle(), + noDataText : Text("No Data"), calculations: .none) } ``` @@ -180,7 +181,7 @@ import SwiftUI - Tag: LineChartData */ public class MultiBarChartData: BarChartDataProtocol { - + // MARK: - Properties public let id : UUID = UUID() @Published public var dataSets : MultiBarDataSet @@ -189,70 +190,77 @@ public class MultiBarChartData: BarChartDataProtocol { @Published public var chartStyle : BarChartStyle @Published public var legends : [LegendData] @Published public var viewData : ChartViewData - @Published public var infoView : InfoViewData = InfoViewData() + @Published public var infoView : InfoViewData = InfoViewData() - public var noDataText : Text = Text("No Data") + public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) - + + // MARK: - Initializers + /// Initialises a multi part Bar Chart with optional calculation + /// + /// Has the option perform optional calculation on the data set, such as averaging based on date. + /// + /// - Note: + /// To add custom calculations use the initialiser with `customCalc`. + /// + /// - 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. + /// - 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. + /// - calculations: Addition calculations that can be performed on the data set before drawing. public init(dataSets : MultiBarDataSet, metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : BarChartStyle = BarChartStyle(), + noDataText : Text = Text("No Data"), calculations: CalculationType = .none ) { self.dataSets = dataSets self.metadata = metadata self.xAxisLabels = xAxisLabels self.chartStyle = chartStyle + self.noDataText = noDataText self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (chartType: .bar, dataSetType: .multi) self.setupLegends() } + /// Initializes a standar Bar Chart with custom calculation + /// + /// Has the option perform custom calculations on the data set. + /// + /// - Note: + /// To add pre built calculations use the initialiser with `calculations`. + /// + /// - 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. + /// - 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. + /// - customCalc: Custom calculations that can be performed on the data set before drawing. public init(dataSets : MultiBarDataSet, metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : BarChartStyle = BarChartStyle(), + noDataText : Text = Text("No Data"), customCalc : @escaping ([BarChartDataPoint]) -> [BarChartDataPoint]? ) { self.dataSets = dataSets self.metadata = metadata self.xAxisLabels = xAxisLabels self.chartStyle = chartStyle + self.noDataText = noDataText self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (chartType: .bar, dataSetType: .multi) self.setupLegends() } - - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { - var points : [BarChartDataPoint] = [] - for dataSet in dataSets.dataSets { - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count) - let index : Int = Int((touchLocation.x) / xSection) - if index >= 0 && index < dataSet.dataPoints.count { - points.append(dataSet.dataPoints[index]) - } - } - return points - } - public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - var locations : [HashablePoint] = [] - for dataSet in dataSets.dataSets { - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count) - let ySection : CGFloat = chartSize.size.height / CGFloat(getMaxValue()) - - let index = Int((touchLocation.x) / xSection) - - if index >= 0 && index < dataSet.dataPoints.count { - locations.append(HashablePoint(x: (CGFloat(index) * xSection) + (xSection / 2), - y: (chartSize.size.height - CGFloat(dataSet.dataPoints[index].value) * ySection))) - } - } - return locations - } + // MARK: - Labels public func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { @@ -297,6 +305,35 @@ public class MultiBarChartData: BarChartDataProtocol { } } + // MARK: - Touch + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { + var points : [BarChartDataPoint] = [] + for dataSet in dataSets.dataSets { + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count) + let index : Int = Int((touchLocation.x) / xSection) + if index >= 0 && index < dataSet.dataPoints.count { + points.append(dataSet.dataPoints[index]) + } + } + return points + } + public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { + var locations : [HashablePoint] = [] + for dataSet in dataSets.dataSets { + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count) + let ySection : CGFloat = chartSize.size.height / CGFloat(getMaxValue()) + + let index = Int((touchLocation.x) / xSection) + + if index >= 0 && index < dataSet.dataPoints.count { + locations.append(HashablePoint(x: (CGFloat(index) * xSection) + (xSection / 2), + y: (chartSize.size.height - CGFloat(dataSet.dataPoints[index].value) * ySection))) + } + } + return locations + } + + // MARK: - Legends public func setupLegends() { switch dataSets.dataSets[0].style.colourFrom { case .barStyle: diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift b/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift index 49406332..6c642ab4 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift @@ -10,35 +10,36 @@ import SwiftUI public struct BarChart: View where ChartData: BarChartData { @ObservedObject var chartData: ChartData - + public init(chartData: ChartData) { self.chartData = chartData } public var body: some View { - - HStack(spacing: 0) { - ForEach(chartData.dataSets.dataPoints) { dataPoint in - - switch chartData.dataSets.style.colourFrom { - case .barStyle: + if chartData.isGreaterThanTwo() { + HStack(spacing: 0) { + ForEach(chartData.dataSets.dataPoints) { dataPoint in - BarChartDataSetSubView(colourType: chartData.dataSets.style.colourType, - dataPoint: dataPoint, - style: chartData.dataSets.style, - chartStyle: chartData.chartStyle, - maxValue: chartData.getMaxValue()) - - case .dataPoints: - - BarChartDataPointSubView(colourType : dataPoint.colourType, - dataPoint : dataPoint, - style : chartData.dataSets.style, - chartStyle : chartData.chartStyle, - maxValue : chartData.getMaxValue()) - + switch chartData.dataSets.style.colourFrom { + case .barStyle: + + BarChartDataSetSubView(colourType: chartData.dataSets.style.colourType, + dataPoint: dataPoint, + style: chartData.dataSets.style, + chartStyle: chartData.chartStyle, + maxValue: chartData.getMaxValue()) + + case .dataPoints: + + BarChartDataPointSubView(colourType : dataPoint.colourType, + dataPoint : dataPoint, + style : chartData.dataSets.style, + chartStyle : chartData.chartStyle, + maxValue : chartData.getMaxValue()) + + } } } - } + } else { CustomNoDataView(chartData: chartData) } } } diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index 95252167..755edf99 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -10,40 +10,45 @@ import SwiftUI public struct GroupedBarChart: View where ChartData: MultiBarChartData { @ObservedObject var chartData: ChartData + + private let groupSpacing : CGFloat - public init(chartData: ChartData) { - self.chartData = chartData + public init(chartData: ChartData, groupSpacing : CGFloat) { + self.chartData = chartData + self.groupSpacing = groupSpacing } public var body: some View { - HStack(spacing: 100) { - ForEach(chartData.dataSets.dataSets) { dataSet in - VStack { - HStack(spacing: 0) { - ForEach(dataSet.dataPoints) { dataPoint in - - switch dataSet.style.colourFrom { - case .barStyle: - - BarChartDataSetSubView(colourType: dataSet.style.colourType, - dataPoint: dataPoint, - style: dataSet.style, - chartStyle: chartData.chartStyle, - maxValue: chartData.getMaxValue()) - - case .dataPoints: - - BarChartDataPointSubView(colourType: dataPoint.colourType, - dataPoint: dataPoint, - style: dataSet.style, - chartStyle: chartData.chartStyle, - maxValue: chartData.getMaxValue()) + if chartData.isGreaterThanTwo() { + HStack(spacing: groupSpacing) { + ForEach(chartData.dataSets.dataSets) { dataSet in + VStack { + HStack(spacing: 0) { + ForEach(dataSet.dataPoints) { dataPoint in + switch dataSet.style.colourFrom { + case .barStyle: + + BarChartDataSetSubView(colourType: dataSet.style.colourType, + dataPoint: dataPoint, + style: dataSet.style, + chartStyle: chartData.chartStyle, + maxValue: chartData.getMaxValue()) + + case .dataPoints: + + BarChartDataPointSubView(colourType: dataPoint.colourType, + dataPoint: dataPoint, + style: dataSet.style, + chartStyle: chartData.chartStyle, + maxValue: chartData.getMaxValue()) + + } } } } } } - } + } else { CustomNoDataView(chartData: chartData) } } } diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index 19808b40..18663b69 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -26,18 +26,19 @@ import SwiftUI LineChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") ], legendTitle: "Data", - pointStyle: PointStyle(), - style: LineStyle()) + pointStyle : PointStyle(), + style : LineStyle()) let metadata = ChartMetadata(title: "Some Data", subtitle: "A Week") let labels = ["Monday", "Thursday", "Sunday"] - return LineChartData(dataSets: data, - metadata: metadata, - xAxisLabels: labels, - chartStyle: LineChartStyle(), - calculations: .none) + return LineChartData(dataSets : data, + metadata : metadata, + xAxisLabels : labels, + chartStyle : LineChartStyle(), + noDataText : Text("No Data"), + calculations : .none) } ``` @@ -155,6 +156,7 @@ import SwiftUI */ public class LineChartData: LineChartDataProtocol { + // MARK: - Properties public let id : UUID = UUID() @Published public var dataSets : LineDataSet @@ -166,9 +168,10 @@ public class LineChartData: LineChartDataProtocol { @Published public var isFilled : Bool = false @Published public var infoView : InfoViewData = InfoViewData() - public var noDataText : Text = Text("No Data") + public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) + // MARK: - Initializers /// Initialises a Single Line Chart with optional calculation /// /// Has the option perform optional calculation on the data set, such as averaging based on date. @@ -181,17 +184,20 @@ public class LineChartData: LineChartDataProtocol { /// - 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. /// - calculations: Addition calculations that can be performed on the data set before drawing. public init(dataSets : LineDataSet, metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : LineChartStyle = LineChartStyle(), + noDataText : Text = Text("No Data"), calculations: CalculationType = .none ) { 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) @@ -206,28 +212,31 @@ public class LineChartData: LineChartDataProtocol { /// To add pre built calculations use the initialiser with `calculations`. /// /// - Parameters: - /// - dataSets: Data to draw a line. + /// - dataSets: Data to draw and style the 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. /// - customCalc: Custom calculations that can be performed on the data set before drawing. public init(dataSets : LineDataSet, metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, - chartStyle : LineChartStyle = LineChartStyle(), + chartStyle : LineChartStyle = LineChartStyle(), + noDataText : Text = Text("No Data"), customCalc : @escaping ([LineChartDataPoint]) -> [LineChartDataPoint]? ) { 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() } - // MARK: Labels + // MARK: - Labels public func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { @@ -271,7 +280,7 @@ public class LineChartData: LineChartDataProtocol { } } - // MARK: Touch + // MARK: - Touch public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [LineChartDataPoint] { var points : [LineChartDataPoint] = [] let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count - 1) @@ -298,7 +307,7 @@ public class LineChartData: LineChartDataProtocol { } return locations } - // MARK: Legends + // MARK: - Legends public func setupLegends() { if dataSets.style.colourType == .colour, diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift index 1795ee58..226f07b3 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -45,10 +45,11 @@ import SwiftUI let metadata = ChartMetadata(title: "Some Data", subtitle: "A Week") let labels = ["Monday", "Thursday", "Sunday"] - return MultiLineChartData(dataSets: data, - metadata: metadata, - xAxisLabels: labels, - chartStyle: LineChartStyle(baseline: .zero), + return MultiLineChartData(dataSets : data, + metadata : metadata, + xAxisLabels : labels, + chartStyle : LineChartStyle(baseline: .zero), + noDataText : Text("No Data"), calculations: .none) } } @@ -167,96 +168,88 @@ import SwiftUI - Tag: LineChartData */ public class MultiLineChartData: LineChartDataProtocol { - + + // MARK: - Properties public let id : UUID = UUID() - /// Data model containing the datapoints: Value, Label, Description and Date. Individual colouring for bar chart. @Published public var dataSets : MultiLineDataSet - - /// 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 : LineChartStyle - - /// Array of data to populate the chart legend. @Published public var legends : [LegendData] - - /// Data model to hold data about the Views layout. @Published public var viewData : ChartViewData @Published public var isFilled : Bool = false - @Published public var infoView : InfoViewData = InfoViewData() - public var noDataText : Text = Text("No Data") - + public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) - + + // MARK: - Initializers + /// Initialises a Multi Line Chart with optional calculation + /// + /// Has the option perform optional calculation on the data set, such as averaging based on date. + /// + /// - Note: + /// To add custom calculations use the initialiser with `customCalc`. + /// + /// - 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. + /// - calculations: Addition calculations that can be performed on the data set before drawing. public init(dataSets : MultiLineDataSet, metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : LineChartStyle = LineChartStyle(), + noDataText : Text = Text("No Data"), calculations: CalculationType = .none ) { 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() } + /// Initializes a Multi Line Chart with custom calculation + /// + /// Has the option perform custom calculations on the data set. + /// + /// - Note: + /// To add pre built calculations use the initialiser with `calculations`. + /// + /// - 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. + /// - customCalc: Custom calculations that can be performed on the data set before drawing. public init(dataSets : MultiLineDataSet, metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : LineChartStyle = LineChartStyle(), + noDataText : Text = Text("No Data"), customCalc : @escaping ([LineChartDataPoint]) -> [LineChartDataPoint]? ) { 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: .multi) self.setupLegends() } - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [LineChartDataPoint] { - var points : [LineChartDataPoint] = [] - for dataSet in dataSets.dataSets { - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) - let index = Int((touchLocation.x + (xSection / 2)) / xSection) - if index >= 0 && index < dataSet.dataPoints.count { - points.append(dataSet.dataPoints[index]) - } - } - return points - } - - public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - - var locations : [HashablePoint] = [] - for dataSet in dataSets.dataSets { - - let minValue : Double = self.getMinValue() - let range : Double = self.getRange() - - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) - let ySection : CGFloat = chartSize.size.height / CGFloat(range) - let index : Int = Int((touchLocation.x + (xSection / 2)) / xSection) - if index >= 0 && index < dataSet.dataPoints.count { - locations.append(HashablePoint(x: CGFloat(index) * xSection, - y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.size.height)) - } - } - return locations - } + // MARK: - Labels public func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { @@ -300,6 +293,38 @@ public class MultiLineChartData: LineChartDataProtocol { } } + // MARK: - Touch + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [LineChartDataPoint] { + var points : [LineChartDataPoint] = [] + for dataSet in dataSets.dataSets { + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) + let index = Int((touchLocation.x + (xSection / 2)) / xSection) + if index >= 0 && index < dataSet.dataPoints.count { + points.append(dataSet.dataPoints[index]) + } + } + return points + } + public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { + + var locations : [HashablePoint] = [] + for dataSet in dataSets.dataSets { + + let minValue : Double = self.getMinValue() + let range : Double = self.getRange() + + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) + let ySection : CGFloat = chartSize.size.height / CGFloat(range) + let index : Int = Int((touchLocation.x + (xSection / 2)) / xSection) + if index >= 0 && index < dataSet.dataPoints.count { + locations.append(HashablePoint(x: CGFloat(index) * xSection, + y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.size.height)) + } + } + return locations + } + + // MARK: - Legends public func setupLegends() { for dataSet in dataSets.dataSets { if dataSet.style.colourType == .colour, diff --git a/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift index b0f9876d..4dc18a6c 100644 --- a/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift @@ -21,18 +21,30 @@ internal struct PointMarkers: ViewModifier where T: LineChartDataProtocol { } internal func body(content: Content) -> some View { ZStack { + if chartData.isGreaterThanTwo() { content - if chartData.chartType.dataSetType == .single { - - let data = chartData as! LineChartData - PointsSubView(dataSets: data.dataSets, minValue: minValue, range: range, animation: chartData.chartStyle.globalAnimation, isFilled: chartData.isFilled) - } else if chartData.chartType.dataSetType == .multi { - - let data = chartData as! MultiLineChartData - ForEach(data.dataSets.dataSets, id: \.self) { dataSet in - PointsSubView(dataSets: dataSet, minValue: minValue, range: range, animation: chartData.chartStyle.globalAnimation, isFilled: chartData.isFilled) + + if chartData.chartType.dataSetType == .single { + + let data = chartData as! LineChartData + PointsSubView(dataSets: data.dataSets, + minValue: minValue, + range: range, + animation: chartData.chartStyle.globalAnimation, + isFilled: chartData.isFilled) + + } else if chartData.chartType.dataSetType == .multi { + + let data = chartData as! MultiLineChartData + ForEach(data.dataSets.dataSets, id: \.self) { dataSet in + PointsSubView(dataSets: dataSet, + minValue: minValue, + range: range, + animation: chartData.chartStyle.globalAnimation, + isFilled: chartData.isFilled) + } } - } + } else { content } } } } diff --git a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift index fb263ff6..d342d503 100644 --- a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift @@ -8,69 +8,68 @@ import SwiftUI public struct FilledLineChart: View where ChartData: LineChartData { - + @ObservedObject var chartData: ChartData - + private let minValue : Double private let range : Double - + @State var startAnimation : Bool = false - + public init(chartData: ChartData) { self.chartData = chartData - self.minValue = chartData.getMinValue() - self.range = chartData.getRange() - + self.minValue = chartData.getMinValue() + self.range = chartData.getRange() self.chartData.isFilled = true } public var body: some View { - -// if chartData.isGreaterThanTwo { - if chartData.dataSets.style.colourType == .colour, - let colour = chartData.dataSets.style.colour - { - - LineChartColourSubView(chartData: chartData, - dataSet: chartData.dataSets, - minValue: minValue, - range: range, - colour: colour, - isFilled: true) + if chartData.isGreaterThanTwo() { - } else if chartData.dataSets.style.colourType == .gradientColour, - let colours = chartData.dataSets.style.colours, - let startPoint = chartData.dataSets.style.startPoint, - let endPoint = chartData.dataSets.style.endPoint - { - - LineChartColoursSubView(chartData: chartData, - dataSet: chartData.dataSets, - minValue: minValue, - range: range, - colours: colours, - startPoint: startPoint, - endPoint: endPoint, - isFilled: true) - - } else if chartData.dataSets.style.colourType == .gradientStops, - let stops = chartData.dataSets.style.stops, - let startPoint = chartData.dataSets.style.startPoint, - let endPoint = chartData.dataSets.style.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) } + if chartData.dataSets.style.colourType == .colour, + let colour = chartData.dataSets.style.colour + { + + LineChartColourSubView(chartData: chartData, + dataSet: chartData.dataSets, + minValue: minValue, + range: range, + colour: colour, + isFilled: true) + + } else if chartData.dataSets.style.colourType == .gradientColour, + let colours = chartData.dataSets.style.colours, + let startPoint = chartData.dataSets.style.startPoint, + let endPoint = chartData.dataSets.style.endPoint + { + + LineChartColoursSubView(chartData: chartData, + dataSet: chartData.dataSets, + minValue: minValue, + range: range, + colours: colours, + startPoint: startPoint, + endPoint: endPoint, + isFilled: true) + + } else if chartData.dataSets.style.colourType == .gradientStops, + let stops = chartData.dataSets.style.stops, + let startPoint = chartData.dataSets.style.startPoint, + let endPoint = chartData.dataSets.style.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/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index 9c6fbd6b..956725f0 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -28,9 +28,9 @@ import SwiftUI around the previous views. ``` .touchOverlay(chartData: data) - .pointMarkers(chartData: data) .averageLine(chartData: data) .yAxisPOI(chartData: data) + .pointMarkers(chartData: data) .xAxisGrid(chartData: data) .yAxisGrid(chartData: data) .xAxisLabels(chartData: data) @@ -101,56 +101,56 @@ public struct LineChart: View where ChartData: LineChartData { public init(chartData: ChartData) { self.chartData = chartData - self.minValue = chartData.getMinValue() - self.range = chartData.getRange() + self.minValue = chartData.getMinValue() + self.range = chartData.getRange() } public var body: some View { -// if chartData.isGreaterThanTwo { - - if chartData.dataSets.style.colourType == .colour, - let colour = chartData.dataSets.style.colour - { - LineChartColourSubView(chartData: chartData, - dataSet : chartData.dataSets, - minValue : minValue, - range : range, - colour : colour, - isFilled : false) - - } else if chartData.dataSets.style.colourType == .gradientColour, - let colours = chartData.dataSets.style.colours, - let startPoint = chartData.dataSets.style.startPoint, - let endPoint = chartData.dataSets.style.endPoint - { - - LineChartColoursSubView(chartData : chartData, - dataSet : chartData.dataSets, - minValue : minValue, - range : range, - colours : colours, - startPoint : startPoint, - endPoint : endPoint, - isFilled : false) - - } else if chartData.dataSets.style.colourType == .gradientStops, - let stops = chartData.dataSets.style.stops, - let startPoint = chartData.dataSets.style.startPoint, - let endPoint = chartData.dataSets.style.endPoint - { - let stops = GradientStop.convertToGradientStopsArray(stops: stops) - - LineChartStopsSubView(chartData : chartData, - dataSet : chartData.dataSets, - minValue : minValue, - range : range, - stops : stops, - startPoint: startPoint, - endPoint : endPoint, - isFilled : false) + if chartData.isGreaterThanTwo() { - } -// } else { CustomNoDataView(chartData: chartData) } + if chartData.dataSets.style.colourType == .colour, + let colour = chartData.dataSets.style.colour + { + LineChartColourSubView(chartData: chartData, + dataSet : chartData.dataSets, + minValue : minValue, + range : range, + colour : colour, + isFilled : false) + + } else if chartData.dataSets.style.colourType == .gradientColour, + let colours = chartData.dataSets.style.colours, + let startPoint = chartData.dataSets.style.startPoint, + let endPoint = chartData.dataSets.style.endPoint + { + + LineChartColoursSubView(chartData : chartData, + dataSet : chartData.dataSets, + minValue : minValue, + range : range, + colours : colours, + startPoint : startPoint, + endPoint : endPoint, + isFilled : false) + + } else if chartData.dataSets.style.colourType == .gradientStops, + let stops = chartData.dataSets.style.stops, + let startPoint = chartData.dataSets.style.startPoint, + let endPoint = chartData.dataSets.style.endPoint + { + let stops = GradientStop.convertToGradientStopsArray(stops: stops) + + LineChartStopsSubView(chartData : chartData, + dataSet : chartData.dataSets, + minValue : minValue, + range : range, + stops : stops, + startPoint: startPoint, + endPoint : endPoint, + isFilled : false) + + } + } else { CustomNoDataView(chartData: chartData) } } } diff --git a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift index b0ce5a23..420951ee 100644 --- a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift @@ -15,65 +15,65 @@ public struct MultiLineChart: View where ChartData: MultiLineChartDat private let range : Double @State var startAnimation : Bool = false - + public init(chartData: ChartData) { self.chartData = chartData - self.minValue = chartData.getMinValue() - self.range = chartData.getRange() + self.minValue = chartData.getMinValue() + self.range = chartData.getRange() } public var body: some View { - ZStack { - ForEach(chartData.dataSets.dataSets, id: \.id) { dataSet in - -// if chartData.isGreaterThanTwo { - - if dataSet.style.colourType == .colour, - let colour = dataSet.style.colour - { - - LineChartColourSubView(chartData: chartData, - dataSet: dataSet, - minValue: minValue, - range: range, - colour: colour, - isFilled: false) - - } else if dataSet.style.colourType == .gradientColour, - let colours = dataSet.style.colours, - let startPoint = dataSet.style.startPoint, - let endPoint = dataSet.style.endPoint - { - - LineChartColoursSubView(chartData: chartData, - dataSet: dataSet, - minValue: minValue, - range: range, - colours: colours, - startPoint: startPoint, - endPoint: endPoint, - isFilled: false) - - } else if dataSet.style.colourType == .gradientStops, - let stops = dataSet.style.stops, - let startPoint = dataSet.style.startPoint, - let endPoint = dataSet.style.endPoint - { - let stops = GradientStop.convertToGradientStopsArray(stops: stops) - - LineChartStopsSubView(chartData: chartData, - dataSet: dataSet, - minValue: minValue, - range: range, - stops: stops, - startPoint: startPoint, - endPoint: endPoint, - isFilled: false) + if chartData.isGreaterThanTwo() { + + ZStack { + ForEach(chartData.dataSets.dataSets, id: \.id) { dataSet in + if dataSet.style.colourType == .colour, + let colour = dataSet.style.colour + { + + LineChartColourSubView(chartData: chartData, + dataSet: dataSet, + minValue: minValue, + range: range, + colour: colour, + isFilled: false) + + } else if dataSet.style.colourType == .gradientColour, + let colours = dataSet.style.colours, + let startPoint = dataSet.style.startPoint, + let endPoint = dataSet.style.endPoint + { + + LineChartColoursSubView(chartData: chartData, + dataSet: dataSet, + minValue: minValue, + range: range, + colours: colours, + startPoint: startPoint, + endPoint: endPoint, + isFilled: false) + + } else if dataSet.style.colourType == .gradientStops, + let stops = dataSet.style.stops, + let startPoint = dataSet.style.startPoint, + let endPoint = dataSet.style.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) } + } else { CustomNoDataView(chartData: chartData) } } } diff --git a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift index 63675a0b..e38c8e77 100644 --- a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift @@ -19,13 +19,12 @@ import SwiftUI public protocol ChartData: ObservableObject, Identifiable { associatedtype Set : DataSet associatedtype DataPoint: CTChartDataPoint - - associatedtype CTStyle : CTChartStyle + associatedtype CTStyle : CTChartStyle var id: ID { get } /** - Data model containing the datapoints. + Data model containing datapoints and styling information. `Set` is either `SingleData` or `MultiDataSet`. */ @@ -129,6 +128,11 @@ public protocol ChartData: ObservableObject, Identifiable { - Tag: setupLegends */ func setupLegends() + + /** + Returns whether there are two or more dataPoints + */ + func isGreaterThanTwo() -> Bool } extension ChartData { @@ -137,6 +141,21 @@ extension ChartData { } } +extension ChartData where Set: SingleDataSet { + public func isGreaterThanTwo() -> Bool { + return dataSets.dataPoints.count > 2 + } +} +extension ChartData where Set: MultiDataSet { + public func isGreaterThanTwo() -> Bool { + var returnValue: Bool = true + dataSets.dataSets.forEach { dataSet in + returnValue = dataSet.dataPoints.count > 2 + } + return returnValue + } +} + // MARK: - Data Sets /** Main protocol set conformace for types of Data Sets. diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index 27c36731..648ede20 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -49,42 +49,42 @@ internal struct HeaderBox: ViewModifier where T: ChartData { internal func body(content: Content) -> some View { -// if chartData.isGreaterThanTwo { - Group { #if !os(tvOS) - if chartData.getHeaderLocation() == .floating { - VStack(alignment: .leading) { - titleBox - content - } - } else if chartData.getHeaderLocation() == .header { - VStack(alignment: .leading) { - HStack(spacing: 0) { - HStack(spacing: 0) { - titleBox - Spacer() - } - .frame(minWidth: 0, maxWidth: .infinity) - Spacer() + if chartData.isGreaterThanTwo() { + if chartData.getHeaderLocation() == .floating { + VStack(alignment: .leading) { + titleBox + content + } + } else if chartData.getHeaderLocation() == .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 } } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift index c95ded4f..58071b58 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift @@ -23,9 +23,13 @@ internal struct Legends: ViewModifier where T: ChartData { } internal func body(content: Content) -> some View { - VStack { - content - LegendView(chartData: chartData, columns: columns, textColor: textColor) + Group { + if chartData.isGreaterThanTwo() { + VStack { + content + LegendView(chartData: chartData, columns: columns, textColor: textColor) + } + } else { content } } } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index bcff57f1..96bfffba 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -46,54 +46,56 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { } internal func body(content: Content) -> some View { -// if chartData.isGreaterThanTwo { - GeometryReader { geo in - ZStack { - content - .gesture( - DragGesture(minimumDistance: 0) - .onChanged { (value) in - touchLocation = value.location - - chartData.infoView.isTouchCurrent = true - - self.selectedPoints = chartData.getDataPoint(touchLocation: touchLocation, - chartSize: geo) - self.pointLocations = chartData.getPointLocation(touchLocation: touchLocation, - chartSize: geo) - if chartData.getHeaderLocation() == .floating { + Group { + if chartData.isGreaterThanTwo() { + GeometryReader { geo in + ZStack { + content + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { (value) in + touchLocation = value.location - setBoxLocationation(boxFrame: boxFrame, chartSize: geo) - markerLocation.x = setMarkerXLocation(chartSize: geo) - markerLocation.y = setMarkerYLocation(chartSize: geo) - - } else if chartData.getHeaderLocation() == .header { + chartData.infoView.isTouchCurrent = true - chartData.infoView.touchOverlayInfo = selectedPoints + self.selectedPoints = chartData.getDataPoint(touchLocation: touchLocation, + chartSize: geo) + self.pointLocations = chartData.getPointLocation(touchLocation: touchLocation, + chartSize: geo) + if chartData.getHeaderLocation() == .floating { + + setBoxLocationation(boxFrame: boxFrame, chartSize: geo) + markerLocation.x = setMarkerXLocation(chartSize: geo) + markerLocation.y = setMarkerYLocation(chartSize: geo) + + } else if chartData.getHeaderLocation() == .header { + + chartData.infoView.touchOverlayInfo = selectedPoints + } } - } - .onEnded { _ in - chartData.infoView.isTouchCurrent = false - chartData.infoView.touchOverlayInfo = [] - } - ) - if chartData.infoView.isTouchCurrent { - ForEach(pointLocations, id: \.self) { location in - TouchOverlayMarker(position: location) - .stroke(Color(.gray), lineWidth: 1) - } - if chartData.getHeaderLocation() == .floating { - TouchOverlayBox(selectedPoints : selectedPoints, - specifier : specifier, - valueColour : chartData.chartStyle.infoBoxValueColour, - descriptionColour: chartData.chartStyle.infoBoxDescriptionColor, - boxFrame : $boxFrame) - .position(x: boxLocation.x, y: 0 + (boxFrame.height / 2)) + .onEnded { _ in + chartData.infoView.isTouchCurrent = false + chartData.infoView.touchOverlayInfo = [] + } + ) + if chartData.infoView.isTouchCurrent { + ForEach(pointLocations, id: \.self) { location in + TouchOverlayMarker(position: location) + .stroke(Color(.gray), lineWidth: 1) + } + if chartData.getHeaderLocation() == .floating { + TouchOverlayBox(selectedPoints : selectedPoints, + specifier : specifier, + valueColour : chartData.chartStyle.infoBoxValueColour, + descriptionColour: chartData.chartStyle.infoBoxDescriptionColor, + boxFrame : $boxFrame) + .position(x: boxLocation.x, y: 0 + (boxFrame.height / 2)) + } } } } - } -// } else { content } + } else { content } + } } /// Sets the point info box location while keeping it within the parent view. diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarProtocols.swift index 29bad9ef..fb3ace67 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarProtocols.swift @@ -19,7 +19,7 @@ import SwiftUI public protocol LineAndBarChartData : ChartData where CTStyle: CTLineAndBarChartStyle { /// Apple's `associatedtype` for outputting `some View`. - associatedtype Body : View + associatedtype Body : View /** Data model to hold data about the Views layout. diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift index 64659c06..7ea28a92 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift @@ -19,25 +19,27 @@ internal struct XAxisBorder: ViewModifier where T: LineAndBarChartData { self.labelsAndBottom = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .bottom } - @ViewBuilder internal func body(content: Content) -> some View { - - if labelsAndBottom { - VStack { - ZStack(alignment: .bottom) { - content - Divider() - } - } - } else if labelsAndTop { - VStack { - ZStack(alignment: .top) { + Group { + if chartData.isGreaterThanTwo() { + if labelsAndBottom { + VStack { + ZStack(alignment: .bottom) { + content + Divider() + } + } + } else if labelsAndTop { + VStack { + ZStack(alignment: .top) { + content + Divider() + } + } + } else { content - Divider() } - } - } else { - content + } else { content } } } } @@ -54,25 +56,25 @@ internal struct YAxisBorder: ViewModifier where T: LineAndBarChartData { self.labelsAndTrailing = chartData.viewData.hasYAxisLabels && chartData.chartStyle.yAxisLabelPosition == .trailing } - @ViewBuilder internal func body(content: Content) -> some View { - - if labelsAndLeading { - HStack { - ZStack(alignment: .leading) { - content - Divider() + Group { + if labelsAndLeading { + HStack { + ZStack(alignment: .leading) { + content + Divider() + } } - } - } else if labelsAndTrailing { - HStack { - ZStack(alignment: .trailing) { - content - Divider() + } else if labelsAndTrailing { + HStack { + ZStack(alignment: .trailing) { + content + Divider() + } } + } else { + content } - } else { - content } } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift index f0e4e799..f20d5639 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift @@ -13,7 +13,7 @@ internal struct XAxisGrid: ViewModifier where T: LineAndBarChartData { internal func body(content: Content) -> some View { ZStack { -// if chartData.isGreaterThanTwo { + if chartData.isGreaterThanTwo() { HStack { ForEach((0...chartData.chartStyle.xAxisGridStyle.numberOfLines), id: \.self) { index in if index != 0 { @@ -24,8 +24,9 @@ internal struct XAxisGrid: ViewModifier where T: LineAndBarChartData { } VerticalGridView(chartData: chartData) } -// } - content + content + } else { content } + } } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift index 4195ccb6..d4fb4053 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift @@ -16,18 +16,23 @@ internal struct XAxisLabels: ViewModifier where T: LineAndBarChartData { self.chartData.viewData.hasXAxisLabels = true } - @ViewBuilder internal func body(content: Content) -> some View { - switch chartData.chartStyle.xAxisLabelPosition { - case .top: - VStack { - chartData.getXAxisLabels() - content - } - case .bottom: - VStack { - content - chartData.getXAxisLabels() + Group { + switch chartData.chartStyle.xAxisLabelPosition { + case .top: + if chartData.isGreaterThanTwo() { + VStack { + chartData.getXAxisLabels() + content + } + } else { content } + case .bottom: + if chartData.isGreaterThanTwo() { + VStack { + content + chartData.getXAxisLabels() + } + } else { content } } } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift index 60a0fad6..4068570f 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift @@ -12,8 +12,8 @@ internal struct YAxisGrid: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData : T internal func body(content: Content) -> some View { - ZStack { -// if chartData.isGreaterThanTwo { + ZStack { + if chartData.isGreaterThanTwo() { VStack { ForEach((0...chartData.chartStyle.yAxisGridStyle.numberOfLines), id: \.self) { index in if index != 0 { @@ -24,8 +24,8 @@ internal struct YAxisGrid: ViewModifier where T: LineAndBarChartData { } HorizontalGridView(chartData: chartData) } -// } - content + content + } else { content } } } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift index 59e84037..19f0f1ad 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift @@ -13,13 +13,19 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { let specifier : String var labelsArray : [Double] { chartData.getYLabels() } - + + let labelsAndTop : Bool + 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 } internal var textAsSpacer: some View { @@ -30,15 +36,12 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { } 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 { + VStack { if labelsAndTop { textAsSpacer } - ForEach((0...numberOfLabels).reversed(), id: \.self) { i in + ForEach((0...chartData.chartStyle.yAxisNumberOfLabels).reversed(), id: \.self) { i in Text("\(labelsArray[i], specifier: specifier)") .font(.caption) .foregroundColor(chartData.chartStyle.yAxisLabelColour) @@ -57,23 +60,22 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { .if(labelsAndTop) { $0.padding(.bottom, -8) } } - @ViewBuilder internal func body(content: Content) -> some View { - switch chartData.chartStyle.yAxisLabelPosition { - case .leading: - HStack { -// if chartData.isGreaterThanTwo { - labels -// } - content - } - case .trailing: - HStack { - content -// if chartData.isGreaterThanTwo { - labels -// } - } + Group { + if chartData.isGreaterThanTwo() { + switch chartData.chartStyle.yAxisLabelPosition { + case .leading: + HStack { + labels + content + } + case .trailing: + HStack { + content + labels + } + } + } else { content } } } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index 78159761..822b643d 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -53,12 +53,13 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { } internal func body(content: Content) -> some View { + ZStack { - content -// if chartData.isGreaterThanTwo { - marker - valueLabel -// } + if chartData.isGreaterThanTwo() { + content + marker + valueLabel + } else { content } } .onAppear { if !chartData.legends.contains(where: { $0.id == uuid }) { // init twice From 15ce09a572867035f533994d5542f3e0c0b84dcd Mon Sep 17 00:00:00 2001 From: Will Dale Date: Tue, 9 Feb 2021 11:31:52 +0000 Subject: [PATCH 033/152] Remove Calculations. --- .../BarChart/Models/BarChartData.swift | 43 +----- .../BarChart/Models/MultiBarChartData.swift | 44 +----- .../LineChart/Models/LineChartData.swift | 58 ++------ .../LineChart/Models/MultiLineChartData.swift | 58 ++------ .../Shared/Extras/Calculations.swift | 139 ------------------ .../Shared/Extras/SharedEnums.swift | 24 --- .../Shared/Models/SharedProtocols.swift | 1 + 7 files changed, 23 insertions(+), 344 deletions(-) delete mode 100644 Sources/SwiftUICharts/Shared/Extras/Calculations.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index d81b5684..a5191142 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -186,24 +186,17 @@ public class BarChartData: BarChartDataProtocol { // MARK: - Initializers /// Initialises a standard Bar Chart with optional calculation /// - /// Has the option perform optional calculation on the data set, such as averaging based on date. - /// - /// - Note: - /// To add custom calculations use the initialiser with `customCalc`. - /// /// - 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. /// - 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. - /// - calculations: Addition calculations that can be performed on the data set before drawing. public init(dataSets : BarDataSet, metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : BarChartStyle = BarChartStyle(), - noDataText : Text = Text("No Data"), - calculations: CalculationType = .none + noDataText : Text = Text("No Data") ) { self.dataSets = dataSets self.metadata = metadata @@ -215,39 +208,7 @@ public class BarChartData: BarChartDataProtocol { self.chartType = (.bar, .single) self.setupLegends() } - - /// Initializes a standar Bar Chart with custom calculation - /// - /// Has the option perform custom calculations on the data set. - /// - /// - Note: - /// To add pre built calculations use the initialiser with `calculations`. - /// - /// - 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. - /// - 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. - /// - customCalc: Custom calculations that can be performed on the data set before drawing. - public init(dataSets : BarDataSet, - metadata : ChartMetadata = ChartMetadata(), - xAxisLabels : [String]? = nil, - chartStyle : BarChartStyle = BarChartStyle(), - noDataText : Text = Text("No Data"), - customCalc : @escaping ([BarChartDataPoint]) -> [BarChartDataPoint]? - ) { - self.dataSets = dataSets - self.metadata = metadata - self.xAxisLabels = xAxisLabels - self.chartStyle = chartStyle - self.noDataText = noDataText - self.legends = [LegendData]() - self.viewData = ChartViewData() - self.chartType = (chartType: .bar, dataSetType: .single) - self.setupLegends() - } - + // MARK: - Labels public func getXAxisLabels() -> some View { Group { diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift index d76aa97c..888a8774 100644 --- a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift @@ -55,8 +55,7 @@ import SwiftUI metadata : ChartMetadata(title: "Hello", subtitle: "Bob"), xAxisLabels : ["Hello"], chartStyle : BarChartStyle(), - noDataText : Text("No Data"), - calculations: .none) + noDataText : Text("No Data")) } ``` @@ -198,56 +197,17 @@ public class MultiBarChartData: BarChartDataProtocol { // MARK: - Initializers /// Initialises a multi part Bar Chart with optional calculation /// - /// Has the option perform optional calculation on the data set, such as averaging based on date. - /// - /// - Note: - /// To add custom calculations use the initialiser with `customCalc`. - /// - /// - 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. - /// - 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. - /// - calculations: Addition calculations that can be performed on the data set before drawing. - public init(dataSets : MultiBarDataSet, - metadata : ChartMetadata = ChartMetadata(), - xAxisLabels : [String]? = nil, - chartStyle : BarChartStyle = BarChartStyle(), - noDataText : Text = Text("No Data"), - calculations: CalculationType = .none - ) { - self.dataSets = dataSets - self.metadata = metadata - self.xAxisLabels = xAxisLabels - self.chartStyle = chartStyle - self.noDataText = noDataText - self.legends = [LegendData]() - self.viewData = ChartViewData() - self.chartType = (chartType: .bar, dataSetType: .multi) - self.setupLegends() - } - - /// Initializes a standar Bar Chart with custom calculation - /// - /// Has the option perform custom calculations on the data set. - /// - /// - Note: - /// To add pre built calculations use the initialiser with `calculations`. - /// /// - 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. /// - 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. - /// - customCalc: Custom calculations that can be performed on the data set before drawing. public init(dataSets : MultiBarDataSet, metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : BarChartStyle = BarChartStyle(), - noDataText : Text = Text("No Data"), - customCalc : @escaping ([BarChartDataPoint]) -> [BarChartDataPoint]? + noDataText : Text = Text("No Data") ) { self.dataSets = dataSets self.metadata = metadata diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index 18663b69..24ba0bff 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -37,8 +37,7 @@ import SwiftUI metadata : metadata, xAxisLabels : labels, chartStyle : LineChartStyle(), - noDataText : Text("No Data"), - calculations : .none) + noDataText : Text("No Data")) } ``` @@ -128,17 +127,17 @@ import SwiftUI --- # Also See - - [Line Data Set](x-source-tag://LineDataSet) - - [Line Chart Data Point](x-source-tag://LineChartDataPoint) - - [Point Style](x-source-tag://PointStyle) + - [LineDataSet](x-source-tag://LineDataSet) + - [LineChartDataPoint](x-source-tag://LineChartDataPoint) + - [PointStyle](x-source-tag://PointStyle) - [PointType](x-source-tag://PointType) - [PointShape](x-source-tag://PointShape) - - [Line Style](x-source-tag://LineStyle) + - [LineStyle](x-source-tag://LineStyle) - [ColourType](x-source-tag://ColourType) - [LineType](x-source-tag://LineType) - [GradientStop](x-source-tag://GradientStop) - - [Chart Metadata](x-source-tag://ChartMetadata) - - [Line Chart Style](x-source-tag://LineChartStyle) + - [ChartMetadata](x-source-tag://ChartMetadata) + - [LineChartStyle](x-source-tag://LineChartStyle) - [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) - [GridStyle](x-source-tag://GridStyle) - [XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) @@ -172,12 +171,7 @@ public class LineChartData: LineChartDataProtocol { public var chartType : (chartType: ChartType, dataSetType: DataSetType) // MARK: - Initializers - /// Initialises a Single Line Chart with optional calculation - /// - /// Has the option perform optional calculation on the data set, such as averaging based on date. - /// - /// - Note: - /// To add custom calculations use the initialiser with `customCalc`. + /// Initialises a Single Line Chart. /// /// - Parameters: /// - dataSets: Data to draw and style a line. @@ -185,45 +179,11 @@ public class LineChartData: LineChartDataProtocol { /// - 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. - /// - calculations: Addition calculations that can be performed on the data set before drawing. - public init(dataSets : LineDataSet, - metadata : ChartMetadata = ChartMetadata(), - xAxisLabels : [String]? = nil, - chartStyle : LineChartStyle = LineChartStyle(), - noDataText : Text = Text("No Data"), - calculations: CalculationType = .none - ) { - 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() - } - - /// Initializes a Single Line Chart with custom calculation - /// - /// Has the option perform custom calculations on the data set. - /// - /// - Note: - /// To add pre built calculations use the initialiser with `calculations`. - /// - /// - Parameters: - /// - dataSets: Data to draw and style the 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. - /// - customCalc: Custom calculations that can be performed on the data set before drawing. public init(dataSets : LineDataSet, metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : LineChartStyle = LineChartStyle(), - noDataText : Text = Text("No Data"), - customCalc : @escaping ([LineChartDataPoint]) -> [LineChartDataPoint]? + noDataText : Text = Text("No Data") ) { self.dataSets = dataSets self.metadata = metadata diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift index 226f07b3..b12b6edc 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -49,8 +49,7 @@ import SwiftUI metadata : metadata, xAxisLabels : labels, chartStyle : LineChartStyle(baseline: .zero), - noDataText : Text("No Data"), - calculations: .none) + noDataText : Text("No Data")) } } @@ -141,17 +140,17 @@ import SwiftUI --- # Also See - - [Line Data Set](x-source-tag://LineDataSet) - - [Line Chart Data Point](x-source-tag://LineChartDataPoint) - - [Point Style](x-source-tag://PointStyle) + - [LineDataSet](x-source-tag://LineDataSet) + - [LineChartDataPoint](x-source-tag://LineChartDataPoint) + - [PointStyle](x-source-tag://PointStyle) - [PointType](x-source-tag://PointType) - [PointShape](x-source-tag://PointShape) - - [Line Style](x-source-tag://LineStyle) + - [LineStyle](x-source-tag://LineStyle) - [ColourType](x-source-tag://ColourType) - [LineType](x-source-tag://LineType) - [GradientStop](x-source-tag://GradientStop) - - [Chart Metadata](x-source-tag://ChartMetadata) - - [Line Chart Style](x-source-tag://LineChartStyle) + - [ChartMetadata](x-source-tag://ChartMetadata) + - [LineChartStyle](x-source-tag://LineChartStyle) - [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) - [GridStyle](x-source-tag://GridStyle) - [XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) @@ -185,12 +184,7 @@ public class MultiLineChartData: LineChartDataProtocol { public var chartType : (chartType: ChartType, dataSetType: DataSetType) // MARK: - Initializers - /// Initialises a Multi Line Chart with optional calculation - /// - /// Has the option perform optional calculation on the data set, such as averaging based on date. - /// - /// - Note: - /// To add custom calculations use the initialiser with `customCalc`. + /// Initialises a Multi Line Chart. /// /// - Parameters: /// - dataSets: Data to draw and style the lines. @@ -198,13 +192,11 @@ public class MultiLineChartData: LineChartDataProtocol { /// - 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. - /// - calculations: Addition calculations that can be performed on the data set before drawing. public init(dataSets : MultiLineDataSet, metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, chartStyle : LineChartStyle = LineChartStyle(), - noDataText : Text = Text("No Data"), - calculations: CalculationType = .none + noDataText : Text = Text("No Data") ) { self.dataSets = dataSets self.metadata = metadata @@ -216,38 +208,6 @@ public class MultiLineChartData: LineChartDataProtocol { self.chartType = (.line, .multi) self.setupLegends() } - - /// Initializes a Multi Line Chart with custom calculation - /// - /// Has the option perform custom calculations on the data set. - /// - /// - Note: - /// To add pre built calculations use the initialiser with `calculations`. - /// - /// - 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. - /// - customCalc: Custom calculations that can be performed on the data set before drawing. - public init(dataSets : MultiLineDataSet, - metadata : ChartMetadata = ChartMetadata(), - xAxisLabels : [String]? = nil, - chartStyle : LineChartStyle = LineChartStyle(), - noDataText : Text = Text("No Data"), - customCalc : @escaping ([LineChartDataPoint]) -> [LineChartDataPoint]? - ) { - 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: .multi) - self.setupLegends() - } // MARK: - Labels public func getXAxisLabels() -> some View { diff --git a/Sources/SwiftUICharts/Shared/Extras/Calculations.swift b/Sources/SwiftUICharts/Shared/Extras/Calculations.swift deleted file mode 100644 index 07f88c45..00000000 --- a/Sources/SwiftUICharts/Shared/Extras/Calculations.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// Calculations.swift -// -// -// Created by Will Dale on 14/01/2021. -// - -import SwiftUI -/** - - Tag: Calculations - */ -//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 numberOfDays = calendar.dateComponents([.day], -// from: firstDataPoint, -// to: lastDataPoint).day else { return nil } -// -// var outputData : [ChartDataPoint] = [] -// for index in 0...numberOfDays { -// 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) -// if !average.isNaN { -// outputData.append(ChartDataPoint(value: average, -// xAxisLabel: formatterForXAxisLabel.string(from: date), -// pointLabel: formatterForPointLabel.string(from: date))) -// } -// } -// } -// return outputData -// } -//} diff --git a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift index b930cd54..d715274c 100644 --- a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift @@ -7,30 +7,6 @@ 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 - ``` - - - Tag: CalculationType - */ -public enum CalculationType { - /// No function - case none - /// Monthly Average - case averageMonth - /// Weekly Average - case averageWeek - /// Daily Average - case averageDay -} - // MARK: - ChartViewData /** The type of `DataSet` being used diff --git a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift index e38c8e77..0344662e 100644 --- a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift @@ -327,4 +327,5 @@ public protocol CTChartDataPoint: Hashable, Identifiable { [See Calculations](x-source-tag://Calculations) */ var date : Date? { get set } + } From c7a9c0a6902551c3ef7716f25b7695a680c5f2e3 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Tue, 9 Feb 2021 12:50:11 +0000 Subject: [PATCH 034/152] Refactor POI legends. --- .../SwiftUICharts/Shared/Views/LegendView.swift | 5 ++--- .../Models/LineAndBarProtocols.swift | 17 ++++++++--------- .../ViewModifiers/YAxisPOI.swift | 4 +--- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index 678c2048..a30f6803 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -10,7 +10,6 @@ import SwiftUI internal struct LegendView: View where T: ChartData { @ObservedObject var chartData : T - private let columns : [GridItem] private let textColor : Color @@ -26,7 +25,7 @@ internal struct LegendView: View where T: ChartData { internal var body: some View { LazyVGrid(columns: columns, alignment: .leading) { - ForEach(chartData.legendOrder(), id: \.id) { legend in + ForEach(chartData.legends) { legend in switch legend.chartType { @@ -44,7 +43,7 @@ internal struct LegendView: View where T: ChartData { .if(chartData.infoView.isTouchCurrent && legend.id == chartData.infoView.touchOverlayInfo[0].id as! UUID) { $0.scaleEffect(1.2, anchor: .leading) } } } - } + }.id(UUID()) } func line(_ legend: LegendData) -> some View { diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarProtocols.swift index fb3ace67..f9028545 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarProtocols.swift @@ -21,15 +21,6 @@ public protocol LineAndBarChartData : ChartData where CTStyle: CTLineAndBarChart /// Apple's `associatedtype` for outputting `some View`. associatedtype Body : View - /** - 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 } - - /** Array of strings for the labels on the X Axis instead of the labels in the data points. @@ -38,6 +29,14 @@ public protocol LineAndBarChartData : ChartData where CTStyle: CTLineAndBarChart */ 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 } + /** Displays a view for the labels on the X Axis. diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index 822b643d..9c85e25e 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -53,7 +53,6 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { } internal func body(content: Content) -> some View { - ZStack { if chartData.isGreaterThanTwo() { content @@ -62,7 +61,7 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { } else { content } } .onAppear { - if !chartData.legends.contains(where: { $0.id == uuid }) { // init twice + if !chartData.legends.contains(where: { $0.legend == markerName }) { // init twice chartData.legends.append(LegendData(id : uuid, legend : markerName, colour : lineColour, @@ -80,7 +79,6 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { maxValue : maxValue, chartType : chartData.chartType.chartType) .stroke(lineColour, style: strokeStyle) - } var valueLabel: some View { From 60746e992868889b7473c6eb178bd24257c9f08e Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 10 Feb 2021 14:12:44 +0000 Subject: [PATCH 035/152] Add point on line indicator. --- .../LineChart/Models/LineChartProtocols.swift | 190 ++++++++++++++++++ .../LineChart/Shapes/LineShape.swift | 20 +- .../Views/SubViews/LineChartSubViews.swift | 3 +- .../Shared/ViewModifiers/TouchOverlay.swift | 74 ++++++- 4 files changed, 267 insertions(+), 20 deletions(-) diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift index 3ccfa1fe..3526f799 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift @@ -20,6 +20,196 @@ import SwiftUI public protocol LineChartDataProtocol: LineAndBarChartData where CTStyle: CTLineChartStyle { var chartStyle : CTStyle { get set } var isFilled : Bool { get set} + + func straightLine(rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, isFilled: Bool) -> Path + func curvedLine(rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, isFilled: Bool) -> Path + + func locationOnPath(_ percent: CGFloat, _ path: Path) -> CGPoint +} + +// MARK: - Paths +extension LineChartDataProtocol { + + public func straightLine(rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, isFilled: 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) + 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 + } + + public func curvedLine(rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, isFilled: 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 + + for index in 1 ..< dataPoints.count { + let nextPoint = CGPoint(x: CGFloat(index) * x, + y: (CGFloat(dataPoints[index].value - minValue) * -y) + rect.height) + + 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 + } + + if isFilled { + // Draw line straight down + path.addLine(to: CGPoint(x: CGFloat(dataPoints.count-1) * 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 + } + + // MARK: - position indicator + // Maybe put all into extentions of: Path / CGPoint / CGFloat + public 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: + print("No reason for this to fire") + + } + } + return total + } + public func getLength(to touch: 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 touch.x < point.x { + isComplete = true + return + } else { + currentPoint = point + } + case .line(to: let nextPoint): + if touch.x < nextPoint.x { + total += lineDistance(from : currentPoint, + to : nextPoint, + touchX: touch.x) + isComplete = true + return + } else { + total += distance(from: currentPoint, to: nextPoint) + currentPoint = nextPoint + } + case .curve(to: let nextPoint, control1: _, control2: _ ): + if touch.x < nextPoint.x { + total += lineDistance(from : currentPoint, + to : nextPoint, + touchX: touch.x) + isComplete = true + return + } else { + total += distance(from: currentPoint, to: nextPoint) + currentPoint = nextPoint + } + case .quadCurve(to: let nextPoint, control: _): + if touch.x < nextPoint.x { + total += lineDistance(from : currentPoint, + to : nextPoint, + touchX: touch.x) + isComplete = true + return + } else { + total += distance(from: currentPoint, to: nextPoint) + currentPoint = nextPoint + } + case .closeSubpath: + print("No reason for this to fire") + + } + } + return total + } + + + 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))) + } + + func lineDistance(from: CGPoint, to: CGPoint, touchX: CGFloat) -> CGFloat { + distance(from: from, to: relativePoint(from: from, to: to, touchX: touchX)) + } + + func distance(from: CGPoint, to: CGPoint) -> CGFloat { + sqrt((from.x - to.x) * (from.x - to.x) + (from.y - to.y) * (from.y - to.y)) + } + + + // https://swiftui-lab.com/swiftui-animations-part2/ + public 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) + } } extension LineAndBarChartData where Self: LineChartDataProtocol { diff --git a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift index c9f7d4ed..fadc96d8 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift @@ -10,13 +10,13 @@ import SwiftUI internal struct LineShape: Shape { private let dataPoints : [LineChartDataPoint] - /// Drawing style of the line private let lineType : LineType private let isFilled : Bool private let minValue : Double private let range : Double + internal init(dataPoints: [LineChartDataPoint], lineType : LineType, isFilled : Bool, @@ -31,30 +31,27 @@ internal struct LineShape: Shape { } internal func path(in rect: CGRect) -> Path { - - let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1) - let y : CGFloat = rect.height / CGFloat(range) switch lineType { case .curvedLine: - return curvedLine(rect, x, y, dataPoints, minValue, range, isFilled) + return curvedLine(rect, dataPoints, minValue, range, isFilled) case .line: - return straightLine(rect, x, y, dataPoints, minValue, range, isFilled) + return straightLine(rect, dataPoints, minValue, range, isFilled) } - } } extension LineShape { func straightLine(_ rect : CGRect, - _ x : CGFloat, - _ y : CGFloat, _ dataPoints : [LineChartDataPoint], _ minValue : Double, _ range : Double, _ isFilled : 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, @@ -73,14 +70,15 @@ extension LineShape { } func curvedLine(_ rect : CGRect, - _ x : CGFloat, - _ y : CGFloat, _ dataPoints : [LineChartDataPoint], _ minValue : Double, _ range : Double, _ isFilled : 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, diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift index 821ee3f3..d6b3cc67 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift @@ -14,9 +14,8 @@ internal struct LineChartColourSubView: View where CD: LineAndBarChartData { let minValue : Double let range : Double let colour : Color - let isFilled : Bool - + @State var startAnimation : Bool = false internal init(chartData : CD, diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 96bfffba..a3d12acd 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -44,10 +44,9 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { self.chartData = chartData self.specifier = specifier } - internal func body(content: Content) -> some View { - Group { - if chartData.isGreaterThanTwo() { +// Group { +// if chartData.isGreaterThanTwo() { GeometryReader { geo in ZStack { content @@ -62,6 +61,7 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { chartSize: geo) self.pointLocations = chartData.getPointLocation(touchLocation: touchLocation, chartSize: geo) + if chartData.getHeaderLocation() == .floating { setBoxLocationation(boxFrame: boxFrame, chartSize: geo) @@ -72,6 +72,7 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { chartData.infoView.touchOverlayInfo = selectedPoints } + } .onEnded { _ in chartData.infoView.isTouchCurrent = false @@ -83,6 +84,56 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { TouchOverlayMarker(position: location) .stroke(Color(.gray), lineWidth: 1) } + + + + // MARK: - position indicator + if chartData.chartType == (.line, .single) { + + let data = chartData as! LineChartData + + + let path = data.curvedLine(rect : geo.frame(in: .global), + dataPoints : data.dataSets.dataPoints, + minValue : data.getMinValue(), + range : data.getRange(), + isFilled : false) + + + let totalLength = data.getTotalLength(of: path) + let lengthToTouch = data.getLength(to: touchLocation, on: path) + let pointLocation = lengthToTouch / totalLength + + + PosistionIndicator() + .frame(width: 5, height: 5) + .position(data.locationOnPath(pointLocation, path)) + } +// else if chartData.chartType == (.line, .multi) { +// +// let data = chartData as! MultiLineChartData +// +// // FOR EACH +// +// ForEach(data.dataSets.dataSets, id: \.self) { dataSet in +// +// let framePercent = (touchLocation.x / geo.size.width) * 100 +// let path = data.straightLine(rect : geo.frame(in: .global), +// dataPoints : dataSet.dataPoints, +// minValue : data.getMinValue(), +// range : data.getRange(), +// isFilled : false) +// +// Image(systemName: "person").resizable().foregroundColor(Color.red) +// .frame(width: 50, height: 50) +// .position(x: data.locationOnPath(framePercent / 100, path).x, +// y: data.locationOnPath(framePercent / 100, path).y) +// } +// +// } + + + if chartData.getHeaderLocation() == .floating { TouchOverlayBox(selectedPoints : selectedPoints, specifier : specifier, @@ -93,8 +144,8 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { } } } - } - } else { content } +// } +// } else { content } } } @@ -135,6 +186,8 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { return touchLocation.y } } + + } #endif @@ -180,5 +233,12 @@ extension View { #endif } - - +struct PosistionIndicator: View { + + var body: some View { + Circle() + .border(Color.secondary, width: 3) + .foregroundColor(.primary) + + } +} From 504202526c1ed792428f3fd138e0c8c4c8d2139a Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 10 Feb 2021 17:12:35 +0000 Subject: [PATCH 036/152] Add Multi line chart point indicator. --- .../LineChart/Models/LineChartProtocols.swift | 6 +- .../LineChart/Shapes/LineShape.swift | 88 ++----------------- .../Views/SubViews/LineChartSubViews.swift | 15 ++-- .../Shared/ViewModifiers/TouchOverlay.swift | 45 +++++----- 4 files changed, 44 insertions(+), 110 deletions(-) diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift index 3526f799..c07e1e6c 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift @@ -31,7 +31,6 @@ public protocol LineChartDataProtocol: LineAndBarChartData where CTStyle: CTLine extension LineChartDataProtocol { public func straightLine(rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, isFilled: Bool) -> Path { - let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1) let y : CGFloat = rect.height / CGFloat(range) @@ -96,9 +95,12 @@ extension LineChartDataProtocol { return path } - +} + +extension LineChartDataProtocol { // MARK: - position indicator // Maybe put all into extentions of: Path / CGPoint / CGFloat + // https://developer.apple.com/documentation/swiftui/path/element public func getTotalLength(of path: Path) -> CGFloat { var total : CGFloat = 0 var currentPoint: CGPoint = .zero diff --git a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift index fadc96d8..b24114f1 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift @@ -7,8 +7,9 @@ import SwiftUI -internal struct LineShape: Shape { +internal struct LineShape: Shape where CD: LineChartDataProtocol { + @ObservedObject var chartData: CD private let dataPoints : [LineChartDataPoint] private let lineType : LineType private let isFilled : Bool @@ -17,13 +18,15 @@ internal struct LineShape: Shape { private let range : Double - internal init(dataPoints: [LineChartDataPoint], + internal init(chartData : CD, + dataPoints: [LineChartDataPoint], lineType : LineType, isFilled : Bool, minValue : Double, range : Double ) { - self.dataPoints = dataPoints + self.chartData = chartData + self.dataPoints = dataPoints self.lineType = lineType self.isFilled = isFilled self.minValue = minValue @@ -34,84 +37,9 @@ internal struct LineShape: Shape { switch lineType { case .curvedLine: - return curvedLine(rect, dataPoints, minValue, range, isFilled) + return chartData.curvedLine(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled) case .line: - return straightLine(rect, dataPoints, minValue, range, isFilled) + return chartData.straightLine(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled) } } } - -extension LineShape { - func straightLine(_ rect : CGRect, - _ dataPoints : [LineChartDataPoint], - _ minValue : Double, - _ range : Double, - _ isFilled : 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) - path.addLine(to: nextPoint) - } - - if isFilled { filled(&path, rect, x, y, dataPoints) } - - return path - } - - func curvedLine(_ rect : CGRect, - _ dataPoints : [LineChartDataPoint], - _ minValue : Double, - _ range : Double, - _ isFilled : 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 - - for index in 1 ..< dataPoints.count { - let nextPoint = CGPoint(x: CGFloat(index) * x, - y: (CGFloat(dataPoints[index].value - minValue) * -y) + rect.height) - - 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 - } - - if isFilled { filled(&path, rect, x, y, dataPoints) } - - return path - } - - func filled(_ path: inout Path, _ rect: CGRect, _ x : CGFloat, _ y : CGFloat, _ dataPoints: [LineChartDataPoint]) { - // Draw line straight down - path.addLine(to: CGPoint(x: CGFloat(dataPoints.count-1) * 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() - } -} diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift index d6b3cc67..fda7d64a 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift @@ -7,7 +7,7 @@ import SwiftUI -internal struct LineChartColourSubView: View where CD: LineAndBarChartData { +internal struct LineChartColourSubView: View where CD: LineChartDataProtocol { let chartData : CD let dataSet : LineDataSet @@ -35,7 +35,8 @@ internal struct LineChartColourSubView: View where CD: LineAndBarChartData { internal var body: some View { - LineShape(dataPoints: dataSet.dataPoints, + LineShape(chartData : chartData, + dataPoints: dataSet.dataPoints, lineType : dataSet.style.lineType, isFilled : isFilled, minValue : minValue, @@ -59,7 +60,7 @@ internal struct LineChartColourSubView: View where CD: LineAndBarChartData { } } -internal struct LineChartColoursSubView: View where CD: LineAndBarChartData { +internal struct LineChartColoursSubView: View where CD: LineChartDataProtocol { let chartData : CD let dataSet : LineDataSet @@ -76,7 +77,8 @@ internal struct LineChartColoursSubView: View where CD: LineAndBarChartData internal var body: some View { - LineShape(dataPoints: dataSet.dataPoints, + LineShape(chartData : chartData, + dataPoints: dataSet.dataPoints, lineType: dataSet.style.lineType, isFilled: isFilled, minValue: minValue, @@ -109,7 +111,7 @@ internal struct LineChartColoursSubView: View where CD: LineAndBarChartData } } -internal struct LineChartStopsSubView: View where CD: LineAndBarChartData { +internal struct LineChartStopsSubView: View where CD: LineChartDataProtocol { let chartData : CD let dataSet : LineDataSet @@ -126,7 +128,8 @@ internal struct LineChartStopsSubView: View where CD: LineAndBarChartData { internal var body: some View { - LineShape(dataPoints: dataSet.dataPoints, + LineShape(chartData : chartData, + dataPoints: dataSet.dataPoints, lineType: dataSet.style.lineType, isFilled: isFilled, minValue: minValue, diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index a3d12acd..51cdccc7 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -108,29 +108,30 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { PosistionIndicator() .frame(width: 5, height: 5) .position(data.locationOnPath(pointLocation, path)) + + } else if chartData.chartType == (.line, .multi) { + + let data = chartData as! MultiLineChartData + + ForEach(data.dataSets.dataSets, id: \.self) { dataSet in + + let path = data.curvedLine(rect : geo.frame(in: .global), + dataPoints : dataSet.dataPoints, + minValue : data.getMinValue(), + range : data.getRange(), + isFilled : false) + + let totalLength = data.getTotalLength(of: path) + let lengthToTouch = data.getLength(to: touchLocation, on: path) + let pointLocation = lengthToTouch / totalLength + + + PosistionIndicator() + .frame(width: 5, height: 5) + .position(data.locationOnPath(pointLocation, path)) + } + } -// else if chartData.chartType == (.line, .multi) { -// -// let data = chartData as! MultiLineChartData -// -// // FOR EACH -// -// ForEach(data.dataSets.dataSets, id: \.self) { dataSet in -// -// let framePercent = (touchLocation.x / geo.size.width) * 100 -// let path = data.straightLine(rect : geo.frame(in: .global), -// dataPoints : dataSet.dataPoints, -// minValue : data.getMinValue(), -// range : data.getRange(), -// isFilled : false) -// -// Image(systemName: "person").resizable().foregroundColor(Color.red) -// .frame(width: 50, height: 50) -// .position(x: data.locationOnPath(framePercent / 100, path).x, -// y: data.locationOnPath(framePercent / 100, path).y) -// } -// -// } From bd244e6c1c1889d17712d24ea450350839293407 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 11 Feb 2021 11:08:57 +0000 Subject: [PATCH 037/152] Move paths into an extension. --- .../LineChart/Extras/PathExtensions.swift | 63 +++++++++++++++++++ .../LineChart/Shapes/LineShape.swift | 12 ++-- .../Views/SubViews/LineChartSubViews.swift | 9 +-- 3 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift diff --git a/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift b/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift new file mode 100644 index 00000000..d06a286a --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift @@ -0,0 +1,63 @@ +// +// PathExtensions.swift +// +// +// Created by Will Dale on 10/02/2021. +// + +import SwiftUI + +// MARK: - Paths +extension Path { + static func straightLine(rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, isFilled: 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) + 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 + } + + static func curvedLine(rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, isFilled: 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 + for index in 1 ..< dataPoints.count { + let nextPoint = CGPoint(x: CGFloat(index) * x, + y: (CGFloat(dataPoints[index].value - minValue) * -y) + rect.height) + + 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 + } + if isFilled { + // Draw line straight down + path.addLine(to: CGPoint(x: CGFloat(dataPoints.count-1) * 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 + } +} diff --git a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift index b24114f1..1ba539d1 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift @@ -7,9 +7,8 @@ import SwiftUI -internal struct LineShape: Shape where CD: LineChartDataProtocol { +internal struct LineShape: Shape { - @ObservedObject var chartData: CD private let dataPoints : [LineChartDataPoint] private let lineType : LineType private let isFilled : Bool @@ -18,14 +17,12 @@ internal struct LineShape: Shape where CD: LineChartDataProtocol { private let range : Double - internal init(chartData : CD, - dataPoints: [LineChartDataPoint], + internal init(dataPoints: [LineChartDataPoint], lineType : LineType, isFilled : Bool, minValue : Double, range : Double ) { - self.chartData = chartData self.dataPoints = dataPoints self.lineType = lineType self.isFilled = isFilled @@ -34,12 +31,11 @@ internal struct LineShape: Shape where CD: LineChartDataProtocol { } internal func path(in rect: CGRect) -> Path { - switch lineType { case .curvedLine: - return chartData.curvedLine(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled) + return Path.curvedLine(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled) case .line: - return chartData.straightLine(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled) + return Path.straightLine(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled) } } } diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift index fda7d64a..15e9dcef 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift @@ -35,8 +35,7 @@ internal struct LineChartColourSubView: View where CD: LineChartDataProtocol internal var body: some View { - LineShape(chartData : chartData, - dataPoints: dataSet.dataPoints, + LineShape(dataPoints: dataSet.dataPoints, lineType : dataSet.style.lineType, isFilled : isFilled, minValue : minValue, @@ -77,8 +76,7 @@ internal struct LineChartColoursSubView: View where CD: LineChartDataProtoco internal var body: some View { - LineShape(chartData : chartData, - dataPoints: dataSet.dataPoints, + LineShape(dataPoints: dataSet.dataPoints, lineType: dataSet.style.lineType, isFilled: isFilled, minValue: minValue, @@ -128,8 +126,7 @@ internal struct LineChartStopsSubView: View where CD: LineChartDataProtocol internal var body: some View { - LineShape(chartData : chartData, - dataPoints: dataSet.dataPoints, + LineShape(dataPoints: dataSet.dataPoints, lineType: dataSet.style.lineType, isFilled: isFilled, minValue: minValue, From 4ac47f9c18de9977159c47ef85fb04f1c48df2d6 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 11 Feb 2021 11:09:19 +0000 Subject: [PATCH 038/152] Tidy up. --- .../LineChart/Models/LineChartProtocols.swift | 223 +++++++++++------- 1 file changed, 135 insertions(+), 88 deletions(-) diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift index c07e1e6c..4d71cd60 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift @@ -21,86 +21,48 @@ public protocol LineChartDataProtocol: LineAndBarChartData where CTStyle: CTLine var chartStyle : CTStyle { get set } var isFilled : Bool { get set} - func straightLine(rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, isFilled: Bool) -> Path - func curvedLine(rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, isFilled: Bool) -> Path + /** + Returns the position to place the indicator on the line + based on the users touch or pointer input. + + - Parameters: + - rect: Frame of the path. + - dataSet: Dataset used to draw the chart. + - touchLocation: Location of the touch or pointer input. + - Returns: The position to place the indicator. + */ + func getIndicatorLocation(rect: CGRect, dataSet: LineDataSet, touchLocation: CGPoint) -> CGPoint - func locationOnPath(_ percent: CGFloat, _ path: Path) -> CGPoint } -// MARK: - Paths +// MARK: - Position Indicator extension LineChartDataProtocol { - public func straightLine(rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, isFilled: Bool) -> Path { - let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1) - let y : CGFloat = rect.height / CGFloat(range) + public func getIndicatorLocation(rect: CGRect, + dataSet: LineDataSet, + touchLocation: CGPoint + ) -> CGPoint { - 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) - path.addLine(to: nextPoint) - } + let path = getPath(style : dataSet.style, + rect : rect, + dataPoints : dataSet.dataPoints, + minValue : self.getMinValue(), + range : self.getRange(), + touchLocation: touchLocation, + isFilled : false) - 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 + return self.locationOnPath(getPercentageOfPath(path: path, touchLocation: touchLocation), path) } - public func curvedLine(rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, isFilled: 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 - - for index in 1 ..< dataPoints.count { - let nextPoint = CGPoint(x: CGFloat(index) * x, - y: (CGFloat(dataPoints[index].value - minValue) * -y) + rect.height) - - 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 - } - - if isFilled { - // Draw line straight down - path.addLine(to: CGPoint(x: CGFloat(dataPoints.count-1) * 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 - } -} - -extension LineChartDataProtocol { - // MARK: - position indicator // Maybe put all into extentions of: Path / CGPoint / CGFloat // https://developer.apple.com/documentation/swiftui/path/element + + /** + The total length of the path. + + - Parameter path: Path to measure. + - Returns: Total length of the path. + */ public func getTotalLength(of path: Path) -> CGFloat { var total : CGFloat = 0 var currentPoint: CGPoint = .zero @@ -119,32 +81,38 @@ extension LineChartDataProtocol { currentPoint = next case .closeSubpath: print("No reason for this to fire") - } } return total } - public func getLength(to touch: CGPoint, on path: Path) -> CGFloat { + + /** + 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. + */ + 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 - } + if isComplete { return } switch element { case .move(to: let point): - if touch.x < point.x { + if touchLocation.x < point.x { isComplete = true return } else { currentPoint = point } case .line(to: let nextPoint): - if touch.x < nextPoint.x { - total += lineDistance(from : currentPoint, + if touchLocation.x < nextPoint.x { + total += distanceToTouch(from : currentPoint, to : nextPoint, - touchX: touch.x) + touchX: touchLocation.x) isComplete = true return } else { @@ -152,10 +120,10 @@ extension LineChartDataProtocol { currentPoint = nextPoint } case .curve(to: let nextPoint, control1: _, control2: _ ): - if touch.x < nextPoint.x { - total += lineDistance(from : currentPoint, + if touchLocation.x < nextPoint.x { + total += distanceToTouch(from : currentPoint, to : nextPoint, - touchX: touch.x) + touchX: touchLocation.x) isComplete = true return } else { @@ -163,10 +131,10 @@ extension LineChartDataProtocol { currentPoint = nextPoint } case .quadCurve(to: let nextPoint, control: _): - if touch.x < nextPoint.x { - total += lineDistance(from : currentPoint, + if touchLocation.x < nextPoint.x { + total += distanceToTouch(from : currentPoint, to : nextPoint, - touchX: touch.x) + touchX: touchLocation.x) isComplete = true return } else { @@ -180,24 +148,102 @@ extension LineChartDataProtocol { } return total } + /** + Returns the relevent path based on the line type. + + - Parameters: + - style: Styling 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 + */ + func getPath(style: LineStyle, rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, touchLocation: CGPoint, isFilled: Bool) -> Path { + switch style.lineType { + case .line: + return Path.straightLine(rect : rect, + dataPoints : dataPoints, + minValue : minValue, + range : range, + isFilled : isFilled) + case .curvedLine: + return Path.curvedLine(rect : rect, + dataPoints : dataPoints, + minValue : minValue, + range : range, + isFilled : isFilled) + } + } + /** + 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. + */ + 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 + } + /** + 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 + */ 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))) } - func lineDistance(from: CGPoint, to: CGPoint, touchX: CGFloat) -> CGFloat { + /** + 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 + */ + 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. + */ func distance(from: CGPoint, to: CGPoint) -> CGFloat { sqrt((from.x - to.x) * (from.x - to.x) + (from.y - to.y) * (from.y - to.y)) } // https://swiftui-lab.com/swiftui-animations-part2/ - public func locationOnPath(_ percent: CGFloat, _ path: Path) -> CGPoint { + /** + Returns a point on the path based on the X axis of the users touch input. + + - Parameters: + - percent: The distance along the path as a percentage. + - path: Path to find location on. + - Returns: Point on path. + */ + func locationOnPath(_ percent: CGFloat, _ path: Path) -> CGPoint { // percent difference between points let diff: CGFloat = 0.001 let comp: CGFloat = 1 - diff @@ -214,6 +260,7 @@ extension LineChartDataProtocol { } } +// MARK: Labels extension LineAndBarChartData where Self: LineChartDataProtocol { public func getYLabels() -> [Double] { var labels : [Double] = [Double]() @@ -229,7 +276,7 @@ extension LineAndBarChartData where Self: LineChartDataProtocol { } } - +// MARK: - Data Functions extension LineAndBarChartData where Self: LineChartData { public func getRange() -> Double { switch self.chartStyle.baseline { @@ -274,7 +321,7 @@ extension LineAndBarChartData where Self: MultiLineChartData { } } } - +// MARK: - Style /** A protocol to extend functionality of `CTLineAndBarChartStyle` specifically for Line Charts. From bf85e360f6c547dca6cdb30bedae5bbf4ec76fbf Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 11 Feb 2021 11:10:23 +0000 Subject: [PATCH 039/152] Tidy up. --- .../Shared/Models/SharedProtocols.swift | 26 ++++--- .../Shared/ViewModifiers/TouchOverlay.swift | 78 +++++++------------ 2 files changed, 42 insertions(+), 62 deletions(-) diff --git a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift index 0344662e..619454f7 100644 --- a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift @@ -100,6 +100,20 @@ public protocol ChartData: ObservableObject, Identifiable { */ func getHeaderLocation() -> InfoBoxPlacement + + /** + Configures the legends based on the type of chart. + + - Tag: setupLegends + */ + func setupLegends() + + /** + Returns whether there are two or more dataPoints + */ + func isGreaterThanTwo() -> Bool + + // MARK: Touch /** Gets the nearest data points to the touch location. - Parameters: @@ -121,18 +135,6 @@ public protocol ChartData: ObservableObject, Identifiable { - Tag: getDataPoint */ func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] - - /** - Configures the legends based on the type of chart. - - - Tag: setupLegends - */ - func setupLegends() - - /** - Returns whether there are two or more dataPoints - */ - func isGreaterThanTwo() -> Bool } extension ChartData { diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 51cdccc7..ccb672c7 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -45,8 +45,8 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { self.specifier = specifier } internal func body(content: Content) -> some View { -// Group { -// if chartData.isGreaterThanTwo() { + Group { + if chartData.isGreaterThanTwo() { GeometryReader { geo in ZStack { content @@ -84,69 +84,49 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { TouchOverlayMarker(position: location) .stroke(Color(.gray), lineWidth: 1) } + if chartData.getHeaderLocation() == .floating { + TouchOverlayBox(selectedPoints : selectedPoints, + specifier : specifier, + valueColour : chartData.chartStyle.infoBoxValueColour, + descriptionColour: chartData.chartStyle.infoBoxDescriptionColor, + boxFrame : $boxFrame) + .position(x: boxLocation.x, y: 0 + (boxFrame.height / 2)) + } - - // MARK: - position indicator + // MARK: - Position Indicator + // TODO: Refactor if chartData.chartType == (.line, .single) { let data = chartData as! LineChartData - - - let path = data.curvedLine(rect : geo.frame(in: .global), - dataPoints : data.dataSets.dataPoints, - minValue : data.getMinValue(), - range : data.getRange(), - isFilled : false) - - - let totalLength = data.getTotalLength(of: path) - let lengthToTouch = data.getLength(to: touchLocation, on: path) - let pointLocation = lengthToTouch / totalLength + let position = data.getIndicatorLocation(rect: geo.frame(in: .global), + dataSet: data.dataSets, + touchLocation: touchLocation) PosistionIndicator() - .frame(width: 5, height: 5) - .position(data.locationOnPath(pointLocation, path)) - + .frame(width: 15, height: 15) + .position(position) + } else if chartData.chartType == (.line, .multi) { let data = chartData as! MultiLineChartData ForEach(data.dataSets.dataSets, id: \.self) { dataSet in - - let path = data.curvedLine(rect : geo.frame(in: .global), - dataPoints : dataSet.dataPoints, - minValue : data.getMinValue(), - range : data.getRange(), - isFilled : false) - - let totalLength = data.getTotalLength(of: path) - let lengthToTouch = data.getLength(to: touchLocation, on: path) - let pointLocation = lengthToTouch / totalLength - - + + let position = data.getIndicatorLocation(rect: geo.frame(in: .global), + dataSet: dataSet, + touchLocation: touchLocation) + PosistionIndicator() - .frame(width: 5, height: 5) - .position(data.locationOnPath(pointLocation, path)) + .frame(width: 15, height: 15) + .position(position) } - - } - - - - if chartData.getHeaderLocation() == .floating { - TouchOverlayBox(selectedPoints : selectedPoints, - specifier : specifier, - valueColour : chartData.chartStyle.infoBoxValueColour, - descriptionColour: chartData.chartStyle.infoBoxDescriptionColor, - boxFrame : $boxFrame) - .position(x: boxLocation.x, y: 0 + (boxFrame.height / 2)) } } } -// } -// } else { content } + } + } else { content } } } @@ -238,8 +218,6 @@ struct PosistionIndicator: View { var body: some View { Circle() - .border(Color.secondary, width: 3) - .foregroundColor(.primary) - + .strokeBorder(Color.red, lineWidth: 3) } } From d700fc4400b6a52547e8689cd828d966ef838438 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Feb 2021 09:32:08 +0000 Subject: [PATCH 040/152] Tidy Up. --- .../BarChart/Models/BarChartData.swift | 7 ++---- .../BarChart/Models/BarChartProtocols.swift | 3 +-- .../LineChart/Models/LineChartProtocols.swift | 10 ++++++++ .../ViewModifiers/YAxisPOI.swift | 23 ++++++++++++++----- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift index a5191142..d9d58e56 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift @@ -262,11 +262,9 @@ public class BarChartData: BarChartDataProtocol { public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { var locations : [HashablePoint] = [] - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count) let ySection : CGFloat = chartSize.size.height / CGFloat(self.getMaxValue()) let index : Int = Int((touchLocation.x) / xSection) - if index >= 0 && index < dataSets.dataPoints.count { locations.append(HashablePoint(x: (CGFloat(index) * xSection) + (xSection / 2), y: (chartSize.size.height - CGFloat(dataSets.dataPoints[index].value) * ySection))) @@ -353,7 +351,6 @@ public class BarChartData: BarChartDataProtocol { } } - public typealias Set = BarDataSet - public typealias DataPoint = BarChartDataPoint - + public typealias Set = BarDataSet + public typealias DataPoint = BarChartDataPoint } diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartProtocols.swift index 353f20c5..ef713157 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartProtocols.swift @@ -50,5 +50,4 @@ extension LineAndBarChartData where Self: BarChartDataProtocol { - Tag: CTBarChartStyle */ -public protocol CTBarChartStyle : CTLineAndBarChartStyle {} - +public protocol CTBarChartStyle: CTLineAndBarChartStyle {} diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift index 4d71cd60..3e3741d0 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift @@ -18,7 +18,17 @@ import SwiftUI - Tag: LineChartDataProtocol */ public protocol LineChartDataProtocol: LineAndBarChartData where CTStyle: CTLineChartStyle { + /** + Data model conatining the style data for the chart. + + # Reference + [CTChartStyle](x-source-tag://CTChartStyle) + */ var chartStyle : CTStyle { get set } + + /** + Whether it is a normal or filled line. + */ var isFilled : Bool { get set} /** diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index 9c85e25e..3981f76a 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -83,9 +83,6 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { var valueLabel: some View { GeometryReader { geo in - - let y = geo.size.height / CGFloat(range) - let pointY = (CGFloat(markerValue - minValue) * -y) + geo.size.height switch labelPosition { case .none: @@ -105,10 +102,10 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { ) .ifElse(self.chartData.chartStyle.yAxisLabelPosition == .leading, if: { $0.position(x: -18, - y: pointY) + y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo)) }, else: { $0.position(x: geo.size.width + 18, - y: pointY) + y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo)) }) @@ -123,10 +120,24 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { .overlay(DiamondShape() .stroke(lineColour, style: strokeStyle) ) - .position(x: geo.size.width / 2, y: pointY) + .position(x: geo.size.width / 2, + y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo)) } } } + + func getYPoint(chartType: ChartType, chartSize: GeometryProxy) -> CGFloat { + switch chartData.chartType.chartType { + case .line: + let y = chartSize.size.height / CGFloat(range) + return (CGFloat(markerValue - minValue) * -y) + chartSize.size.height + case .bar: + let y = chartSize.size.height / CGFloat(maxValue) + return chartSize.size.height - CGFloat(markerValue) * y + case .pie: + return 0 + } + } } extension View { From 1ca16b6cc6e6ddfc0a8a60d9cd4e51cd72deb8b2 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Feb 2021 09:32:35 +0000 Subject: [PATCH 041/152] Fix touch overlay for Grouped Bar charts. --- .../BarChart/Models/MultiBarChartData.swift | 134 +++++++++++------- .../BarChart/Views/GroupedBarChart.swift | 42 +++--- 2 files changed, 106 insertions(+), 70 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift index 888a8774..ed01eadf 100644 --- a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift @@ -194,6 +194,8 @@ public class MultiBarChartData: BarChartDataProtocol { public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) + var groupSpacing : CGFloat = 0 + // MARK: - Initializers /// Initialises a multi part Bar Chart with optional calculation /// @@ -221,75 +223,109 @@ public class MultiBarChartData: BarChartDataProtocol { } // MARK: - Labels + @ViewBuilder public func getXAxisLabels() -> some View { - Group { - switch self.chartStyle.xAxisLabelsFrom { - case .dataPoint: - - HStack(spacing: 100) { - ForEach(dataSets.dataSets) { dataSet in - HStack(spacing: 0) { - ForEach(dataSet.dataPoints) { data in - Text(data.xAxisLabel ?? "") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) - if data != dataSet.dataPoints[dataSet.dataPoints.count - 1] { - Spacer() - .frame(minWidth: 0, maxWidth: 500) - } - } - } - } - } - .padding(.horizontal, -4) - - case .chartData: - - if let labelArray = self.xAxisLabels { + switch self.chartStyle.xAxisLabelsFrom { + case .dataPoint: + HStack(spacing: 100) { + ForEach(dataSets.dataSets) { dataSet in HStack(spacing: 0) { - ForEach(labelArray, id: \.self) { data in - Spacer() - .frame(minWidth: 0, maxWidth: 500) - Text(data) + ForEach(dataSet.dataPoints) { data in + Text(data.xAxisLabel ?? "") .font(.caption) - .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - Spacer() - .frame(minWidth: 0, maxWidth: 500) + if data != dataSet.dataPoints[dataSet.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 + Spacer() + .frame(minWidth: 0, maxWidth: 500) + Text(data) + .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) + .lineLimit(1) + .minimumScaleFactor(0.5) + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } } } // MARK: - Touch public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { + var points : [BarChartDataPoint] = [] - for dataSet in dataSets.dataSets { - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count) - let index : Int = Int((touchLocation.x) / xSection) - if index >= 0 && index < dataSet.dataPoints.count { - points.append(dataSet.dataPoints[index]) + + // Divide the chart into equal sections. + let superXSection : CGFloat = (chartSize.size.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.size.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 { + points.append(dataSet.dataPoints[subIndex]) } } return points } +// public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { +// +// var points : [BarChartDataPoint] = [] +// +// // Divide the chart into equal sections. +// let superXSection : CGFloat = (chartSize.size.width / CGFloat(dataSets.dataSets.count)) +// let superIndex : Int = Int((touchLocation.x) / superXSection) +// +// // Make those sections take account of spacing between groups. +// let xSection : CGFloat = (chartSize.size.width / CGFloat(dataSets.dataSets.count)) - 75 +// let index : Int = Int((touchLocation.x - CGFloat((100 * 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((100 * superIndex))) / xSubSection) - (dataSet.dataPoints.count * index) +// if subIndex >= 0 && subIndex < dataSet.dataPoints.count { +// points.append(dataSet.dataPoints[subIndex]) +// } +// } +// return points +// } public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - var locations : [HashablePoint] = [] - for dataSet in dataSets.dataSets { - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count) - let ySection : CGFloat = chartSize.size.height / CGFloat(getMaxValue()) - - let index = Int((touchLocation.x) / xSection) - - if index >= 0 && index < dataSet.dataPoints.count { - locations.append(HashablePoint(x: (CGFloat(index) * xSection) + (xSection / 2), - y: (chartSize.size.height - CGFloat(dataSet.dataPoints[index].value) * ySection))) - } - } + let locations : [HashablePoint] = [] +// for dataSet in dataSets.dataSets { +// let xSection : CGFloat = (chartSize.size.width / CGFloat(dataSet.dataPoints.count)) +// let ySection : CGFloat = chartSize.size.height / CGFloat(getMaxValue()) +// +// let index = Int((touchLocation.x) / xSection) +// +// if index >= 0 && index < dataSet.dataPoints.count { +// locations.append(HashablePoint(x: (CGFloat(index) * xSection) + (xSection / 2), +// y: (chartSize.size.height - CGFloat(dataSet.dataPoints[index].value) * ySection))) +// } +// } return locations } diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index 755edf99..b244e4a3 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -16,34 +16,34 @@ public struct GroupedBarChart: View where ChartData: MultiBarChartDat public init(chartData: ChartData, groupSpacing : CGFloat) { self.chartData = chartData self.groupSpacing = groupSpacing + + self.chartData.groupSpacing = groupSpacing } public var body: some View { if chartData.isGreaterThanTwo() { HStack(spacing: groupSpacing) { ForEach(chartData.dataSets.dataSets) { dataSet in - VStack { - HStack(spacing: 0) { - ForEach(dataSet.dataPoints) { dataPoint in + HStack(spacing: 0) { + ForEach(dataSet.dataPoints) { dataPoint in + + switch dataSet.style.colourFrom { + case .barStyle: + + BarChartDataSetSubView(colourType: dataSet.style.colourType, + dataPoint: dataPoint, + style: dataSet.style, + chartStyle: chartData.chartStyle, + maxValue: chartData.getMaxValue()) + + case .dataPoints: + + BarChartDataPointSubView(colourType: dataPoint.colourType, + dataPoint: dataPoint, + style: dataSet.style, + chartStyle: chartData.chartStyle, + maxValue: chartData.getMaxValue()) - switch dataSet.style.colourFrom { - case .barStyle: - - BarChartDataSetSubView(colourType: dataSet.style.colourType, - dataPoint: dataPoint, - style: dataSet.style, - chartStyle: chartData.chartStyle, - maxValue: chartData.getMaxValue()) - - case .dataPoints: - - BarChartDataPointSubView(colourType: dataPoint.colourType, - dataPoint: dataPoint, - style: dataSet.style, - chartStyle: chartData.chartStyle, - maxValue: chartData.getMaxValue()) - - } } } } From 8468037399a58e4092085cebb096d673f77a490b Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sat, 13 Feb 2021 11:02:46 +0000 Subject: [PATCH 042/152] Add stacked bar chart. --- .../Models/{ => ChartData}/BarChartData.swift | 0 .../GroupedBarChartData.swift} | 47 +--- .../ChartData/StackedBarChartData.swift | 218 ++++++++++++++++++ .../Models/{ => DataSet}/BarDataSet.swift | 0 .../{ => DataSet}/MultiBarDataSet.swift | 0 .../{BarChartView.swift => BarChart.swift} | 2 +- .../BarChart/Views/GroupedBarChart.swift | 2 +- .../BarChart/Views/StackedBarChart.swift | 65 ++++++ .../SubViews/BarChartDataSetSubView.swift | 7 +- .../LineChart/Models/LineChartData.swift | 22 ++ .../LineChart/Models/LineChartProtocols.swift | 46 ---- .../LineChart/Models/MultiLineChartData.swift | 22 ++ 12 files changed, 343 insertions(+), 88 deletions(-) rename Sources/SwiftUICharts/BarChart/Models/{ => ChartData}/BarChartData.swift (100%) rename Sources/SwiftUICharts/BarChart/Models/{MultiBarChartData.swift => ChartData/GroupedBarChartData.swift} (88%) create mode 100644 Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift rename Sources/SwiftUICharts/BarChart/Models/{ => DataSet}/BarDataSet.swift (100%) rename Sources/SwiftUICharts/BarChart/Models/{ => DataSet}/MultiBarDataSet.swift (100%) rename Sources/SwiftUICharts/BarChart/Views/{BarChartView.swift => BarChart.swift} (98%) create mode 100644 Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift similarity index 100% rename from Sources/SwiftUICharts/BarChart/Models/BarChartData.swift rename to Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift similarity index 88% rename from Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift rename to Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index ed01eadf..f4aacb63 100644 --- a/Sources/SwiftUICharts/BarChart/Models/MultiBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -14,7 +14,7 @@ import SwiftUI # Example ``` - static func makeData() -> MultiBarChartData { + static func makeData() -> GroupedBarChartData { let data = MultiBarDataSet(dataSets: [ BarDataSet(dataPoints: [ @@ -51,7 +51,7 @@ import SwiftUI style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)) ]) - return MultiBarChartData(dataSets : data, + return GroupedBarChartData(dataSets : data, metadata : ChartMetadata(title: "Hello", subtitle: "Bob"), xAxisLabels : ["Hello"], chartStyle : BarChartStyle(), @@ -177,9 +177,9 @@ import SwiftUI - LineAndBarChartData - ChartData - - Tag: LineChartData + - Tag: GroupedBarChartData */ -public class MultiBarChartData: BarChartDataProtocol { +public class GroupedBarChartData: BarChartDataProtocol { // MARK: - Properties public let id : UUID = UUID() @@ -291,41 +291,9 @@ public class MultiBarChartData: BarChartDataProtocol { } return points } -// public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { -// -// var points : [BarChartDataPoint] = [] -// -// // Divide the chart into equal sections. -// let superXSection : CGFloat = (chartSize.size.width / CGFloat(dataSets.dataSets.count)) -// let superIndex : Int = Int((touchLocation.x) / superXSection) -// -// // Make those sections take account of spacing between groups. -// let xSection : CGFloat = (chartSize.size.width / CGFloat(dataSets.dataSets.count)) - 75 -// let index : Int = Int((touchLocation.x - CGFloat((100 * 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((100 * superIndex))) / xSubSection) - (dataSet.dataPoints.count * index) -// if subIndex >= 0 && subIndex < dataSet.dataPoints.count { -// points.append(dataSet.dataPoints[subIndex]) -// } -// } -// return points -// } + public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { let locations : [HashablePoint] = [] -// for dataSet in dataSets.dataSets { -// let xSection : CGFloat = (chartSize.size.width / CGFloat(dataSet.dataPoints.count)) -// let ySection : CGFloat = chartSize.size.height / CGFloat(getMaxValue()) -// -// let index = Int((touchLocation.x) / xSection) -// -// if index >= 0 && index < dataSet.dataPoints.count { -// locations.append(HashablePoint(x: (CGFloat(index) * xSection) + (xSection / 2), -// y: (chartSize.size.height - CGFloat(dataSet.dataPoints[index].value) * ySection))) -// } -// } return locations } @@ -408,7 +376,8 @@ public class MultiBarChartData: BarChartDataProtocol { } } - public typealias Set = MultiBarDataSet - public typealias DataPoint = BarChartDataPoint + public typealias Set = MultiBarDataSet + public typealias DataPoint = BarChartDataPoint + public typealias CTStyle = BarChartStyle } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift new file mode 100644 index 00000000..eabb6fc7 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -0,0 +1,218 @@ +// +// StackedBarChartData.swift +// +// +// Created by Will Dale on 12/02/2021. +// + +import SwiftUI + +public class StackedBarChartData: BarChartDataProtocol { + + // MARK: - Properties + public let id : UUID = UUID() + + @Published public var dataSets : MultiBarDataSet + @Published public var metadata : ChartMetadata + @Published public var xAxisLabels : [String]? + @Published public var chartStyle : BarChartStyle + @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) + + public init(dataSets : MultiBarDataSet, + metadata : ChartMetadata = ChartMetadata(), + xAxisLabels : [String]? = nil, + chartStyle : BarChartStyle = BarChartStyle(), + 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: .bar, dataSetType: .multi) + self.setupLegends() + } + // MARK: - Labels + @ViewBuilder + public func getXAxisLabels() -> some View { + switch self.chartStyle.xAxisLabelsFrom { + case .dataPoint: + HStack(spacing: 100) { + ForEach(dataSets.dataSets) { dataSet in + HStack(spacing: 0) { + ForEach(dataSet.dataPoints) { data in + Text(data.xAxisLabel ?? "") + .font(.caption) + .lineLimit(1) + .minimumScaleFactor(0.5) + if data != dataSet.dataPoints[dataSet.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 + Spacer() + .frame(minWidth: 0, maxWidth: 500) + Text(data) + .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) + .lineLimit(1) + .minimumScaleFactor(0.5) + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } + } + } + + // MARK: - Touch + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { + + var points : [BarChartDataPoint] = [] + + // Filter to get the right dataset based on the x axis. + let superXSection : CGFloat = chartSize.size.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.getMaxValue() + 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.size.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.size.height) }) + + if let index = yIndex?.offset { + if index >= 0 && index < dataSet.dataPoints.count { + points.append(dataSet.dataPoints[index]) + } + } + } + return points + } + + public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { + let locations : [HashablePoint] = [] + return locations + } + + // MARK: - Legends + public func setupLegends() { + switch dataSets.dataSets[0].style.colourFrom { + case .barStyle: + if dataSets.dataSets[0].style.colourType == .colour, + let colour = dataSets.dataSets[0].style.colour + { + self.legends.append(LegendData(id : dataSets.dataSets[0].id, + legend : dataSets.dataSets[0].legendTitle, + colour : colour, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if dataSets.dataSets[0].style.colourType == .gradientColour, + let colours = dataSets.dataSets[0].style.colours + { + self.legends.append(LegendData(id : dataSets.dataSets[0].id, + legend : dataSets.dataSets[0].legendTitle, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if dataSets.dataSets[0].style.colourType == .gradientStops, + let stops = dataSets.dataSets[0].style.stops + { + self.legends.append(LegendData(id : dataSets.dataSets[0].id, + legend : dataSets.dataSets[0].legendTitle, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } + case .dataPoints: + + for data in dataSets.dataSets[0].dataPoints { + + if data.colourType == .colour, + let colour = data.colour, + let legend = data.pointDescription + { + self.legends.append(LegendData(id : data.id, + legend : legend, + colour : colour, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if data.colourType == .gradientColour, + let colours = data.colours, + let legend = data.pointDescription + { + self.legends.append(LegendData(id : data.id, + legend : legend, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if data.colourType == .gradientStops, + let stops = data.stops, + let legend = data.pointDescription + { + self.legends.append(LegendData(id : data.id, + legend : legend, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } + } + } + } + public typealias Set = MultiBarDataSet + public typealias DataPoint = BarChartDataPoint + public typealias CTStyle = BarChartStyle +} diff --git a/Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift similarity index 100% rename from Sources/SwiftUICharts/BarChart/Models/BarDataSet.swift rename to Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/MultiBarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSet.swift similarity index 100% rename from Sources/SwiftUICharts/BarChart/Models/MultiBarDataSet.swift rename to Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSet.swift diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift similarity index 98% rename from Sources/SwiftUICharts/BarChart/Views/BarChartView.swift rename to Sources/SwiftUICharts/BarChart/Views/BarChart.swift index 6c642ab4..7e7c6221 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChartView.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift @@ -1,5 +1,5 @@ // -// BarChartView.swift +// BarChart.swift // // // Created by Will Dale on 11/01/2021. diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index b244e4a3..9e29a77f 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -7,7 +7,7 @@ import SwiftUI -public struct GroupedBarChart: View where ChartData: MultiBarChartData { +public struct GroupedBarChart: View where ChartData: GroupedBarChartData { @ObservedObject var chartData: ChartData diff --git a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift new file mode 100644 index 00000000..c50612fe --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift @@ -0,0 +1,65 @@ +// +// StackedBarChart.swift +// +// +// Created by Will Dale on 12/02/2021. +// + +import SwiftUI + + +public struct StackedBarChart: View where ChartData: StackedBarChartData { + + @ObservedObject var chartData: ChartData + + public init(chartData: ChartData) { + self.chartData = chartData + } + + public var body: some View { + + if chartData.isGreaterThanTwo() { + + HStack(alignment: .bottom, spacing: 0) { + ForEach(chartData.dataSets.dataSets) { dataSet in + + MultiPartBarView(dataSet: dataSet) + .scaleEffect(y: CGFloat(DataFunctions.dataSetMaxValue(from: dataSet) / chartData.getMaxValue()), + anchor: .bottom) + } + } + + } else { CustomNoDataView(chartData: chartData) } + } +} + +struct MultiPartBarView: View { + + let dataSet : BarDataSet + + init(dataSet: BarDataSet) { + self.dataSet = dataSet + } + + var body: some View { + GeometryReader { geo in + + VStack(spacing: 0) { + ForEach(dataSet.dataPoints.reversed()) { datapoint in + + Rectangle() + .fill(datapoint.colour ?? .pink) + .frame(height: getHeight(height: geo.size.height, + dataSet: dataSet, + dataPoint: datapoint)) + } + } + } + } + + func getHeight(height: CGFloat, dataSet: BarDataSet, dataPoint: BarChartDataPoint) -> CGFloat { + let value = dataPoint.value + let sum = dataSet.dataPoints.reduce(0) { $0 + $1.value } + return height * CGFloat(value / sum) + } +} diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartDataSetSubView.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartDataSetSubView.swift index bd9edede..aa5e97ce 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartDataSetSubView.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartDataSetSubView.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Bar segment where the colour information comes from chart style. + */ internal struct BarChartDataSetSubView: View { let colourType : ColourType @@ -42,7 +45,9 @@ internal struct BarChartDataSetSubView: View { } } - +/** + Bar segment where the colour information comes from datapoints. + */ internal struct BarChartDataPointSubView: View { let colourType : ColourType diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift index 24ba0bff..cfa90273 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift @@ -306,6 +306,28 @@ public class LineChartData: LineChartDataProtocol { } } + // MARK: - Data Functions + public func getRange() -> Double { + switch self.chartStyle.baseline { + case .minimumValue: + return DataFunctions.dataSetRange(from: dataSets) + case .minimumWithMaximum(of: let value): + return DataFunctions.dataSetMaxValue(from: dataSets) - min(DataFunctions.dataSetMinValue(from: dataSets), value) + case .zero: + return DataFunctions.dataSetMaxValue(from: dataSets) + } + } + public func getMinValue() -> Double { + switch self.chartStyle.baseline { + case .minimumValue: + return DataFunctions.dataSetMinValue(from: dataSets) + case .minimumWithMaximum(of: let value): + return min(DataFunctions.dataSetMinValue(from: dataSets), value) + case .zero: + return 0 + } + } + public typealias Set = LineDataSet public typealias DataPoint = LineChartDataPoint } diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift index 3e3741d0..0e350e54 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift @@ -285,52 +285,6 @@ extension LineAndBarChartData where Self: LineChartDataProtocol { return labels } } - -// MARK: - Data Functions -extension LineAndBarChartData where Self: LineChartData { - public func getRange() -> Double { - switch self.chartStyle.baseline { - case .minimumValue: - return DataFunctions.dataSetRange(from: dataSets) - case .minimumWithMaximum(of: let value): - return DataFunctions.dataSetMaxValue(from: dataSets) - min(DataFunctions.dataSetMinValue(from: dataSets), value) - case .zero: - return DataFunctions.dataSetMaxValue(from: dataSets) - } - } - public func getMinValue() -> Double { - switch self.chartStyle.baseline { - case .minimumValue: - return DataFunctions.dataSetMinValue(from: dataSets) - case .minimumWithMaximum(of: let value): - return min(DataFunctions.dataSetMinValue(from: dataSets), value) - case .zero: - return 0 - } - } -} -extension LineAndBarChartData where Self: MultiLineChartData { - public func getRange() -> Double { - switch self.chartStyle.baseline { - case .minimumValue: - return DataFunctions.multiDataSetRange(from: dataSets) - case .minimumWithMaximum(of: let value): - return DataFunctions.multiDataSetMaxValue(from: dataSets) - min(DataFunctions.multiDataSetMinValue(from: dataSets), value) - case .zero: - return DataFunctions.multiDataSetMaxValue(from: dataSets) - } - } - public func getMinValue() -> Double { - switch self.chartStyle.baseline { - case .minimumValue: - return DataFunctions.multiDataSetMinValue(from: dataSets) - case .minimumWithMaximum(of: let value): - return min(DataFunctions.multiDataSetMinValue(from: dataSets), value) - case .zero: - return 0 - } - } -} // MARK: - Style /** A protocol to extend functionality of `CTLineAndBarChartStyle` specifically for Line Charts. diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift index b12b6edc..af1242c4 100644 --- a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift @@ -324,6 +324,28 @@ public class MultiLineChartData: LineChartDataProtocol { } } + // MARK: - Data Functions + public func getRange() -> Double { + switch self.chartStyle.baseline { + case .minimumValue: + return DataFunctions.multiDataSetRange(from: dataSets) + case .minimumWithMaximum(of: let value): + return DataFunctions.multiDataSetMaxValue(from: dataSets) - min(DataFunctions.multiDataSetMinValue(from: dataSets), value) + case .zero: + return DataFunctions.multiDataSetMaxValue(from: dataSets) + } + } + public func getMinValue() -> Double { + switch self.chartStyle.baseline { + case .minimumValue: + return DataFunctions.multiDataSetMinValue(from: dataSets) + case .minimumWithMaximum(of: let value): + return min(DataFunctions.multiDataSetMinValue(from: dataSets), value) + case .zero: + return 0 + } + } + public typealias Set = MultiLineDataSet public typealias DataPoint = LineChartDataPoint } From 1a0f1fc312adca4e0bff6780b77b19c84e60786f Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sat, 13 Feb 2021 11:28:51 +0000 Subject: [PATCH 043/152] Restructure folders. Remove PointStyle from bar charts. Add new protocol for line chart data set. --- .../{Models => Extras}/BarChartEnums.swift | 0 .../Models/ChartData/BarChartData.swift | 1 - .../Models/ChartData/GroupedBarChartData.swift | 4 ---- .../BarChart/Models/DataSet/BarDataSet.swift | 8 +------- .../Models/{ => Style}/BarChartStyle.swift | 0 .../BarChart/Models/{ => Style}/BarStyle.swift | 0 .../{Models => Types}/CornerRadius.swift | 0 .../{Models => Extras}/LineChartEnums.swift | 0 .../Models/{ => ChartData}/LineChartData.swift | 0 .../{ => ChartData}/MultiLineChartData.swift | 0 .../Models/{ => DataSet}/LineDataSet.swift | 2 +- .../{ => DataSet}/MultiLineDataSet.swift | 0 .../LineChart/Models/LineChartProtocols.swift | 18 ++++++++++++++++++ .../Models/{ => Style}/LineChartStyle.swift | 0 .../Models/{ => Style}/LineStyle.swift | 0 .../Models/{ => Style}/PointStyle.swift | 0 .../LineChart/Shapes/PointShape.swift | 2 +- .../Shared/Models/SharedProtocols.swift | 10 ++-------- 18 files changed, 23 insertions(+), 22 deletions(-) rename Sources/SwiftUICharts/BarChart/{Models => Extras}/BarChartEnums.swift (100%) rename Sources/SwiftUICharts/BarChart/Models/{ => Style}/BarChartStyle.swift (100%) rename Sources/SwiftUICharts/BarChart/Models/{ => Style}/BarStyle.swift (100%) rename Sources/SwiftUICharts/BarChart/{Models => Types}/CornerRadius.swift (100%) rename Sources/SwiftUICharts/LineChart/{Models => Extras}/LineChartEnums.swift (100%) rename Sources/SwiftUICharts/LineChart/Models/{ => ChartData}/LineChartData.swift (100%) rename Sources/SwiftUICharts/LineChart/Models/{ => ChartData}/MultiLineChartData.swift (100%) rename Sources/SwiftUICharts/LineChart/Models/{ => DataSet}/LineDataSet.swift (98%) rename Sources/SwiftUICharts/LineChart/Models/{ => DataSet}/MultiLineDataSet.swift (100%) rename Sources/SwiftUICharts/LineChart/Models/{ => Style}/LineChartStyle.swift (100%) rename Sources/SwiftUICharts/LineChart/Models/{ => Style}/LineStyle.swift (100%) rename Sources/SwiftUICharts/LineChart/Models/{ => Style}/PointStyle.swift (100%) diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartEnums.swift b/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift similarity index 100% rename from Sources/SwiftUICharts/BarChart/Models/BarChartEnums.swift rename to Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index d9d58e56..0039b2d6 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -27,7 +27,6 @@ import SwiftUI BarChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") ], legendTitle: "Data", - pointStyle: PointStyle(), style: BarStyle()) let metadata : ChartMetadata = ChartMetadata(title : "Test Data", diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index f4aacb63..f69d1730 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -23,7 +23,6 @@ import SwiftUI BarChartDataPoint(value: 30, xAxisLabel: "1.3", pointLabel: "One Three", colour: .purple), BarChartDataPoint(value: 40, xAxisLabel: "1.4", pointLabel: "One Four" , colour: .green)], legendTitle: "One", - pointStyle: PointStyle(), style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)), BarDataSet(dataPoints: [ BarChartDataPoint(value: 50, xAxisLabel: "2.1", pointLabel: "Two One" , colour: .blue), @@ -31,7 +30,6 @@ import SwiftUI BarChartDataPoint(value: 40, xAxisLabel: "2.3", pointLabel: "Two Three", colour: .purple), BarChartDataPoint(value: 60, xAxisLabel: "2.3", pointLabel: "Two Three", colour: .green)], legendTitle: "Two", - pointStyle: PointStyle(), style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)), BarDataSet(dataPoints: [ BarChartDataPoint(value: 10, xAxisLabel: "3.1", pointLabel: "Three One" , colour: .blue), @@ -39,7 +37,6 @@ import SwiftUI BarChartDataPoint(value: 30, xAxisLabel: "3.3", pointLabel: "Three Three", colour: .purple), BarChartDataPoint(value: 99, xAxisLabel: "3.4", pointLabel: "Three Four" , colour: .green)], legendTitle: "Three", - pointStyle: PointStyle(), style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)), BarDataSet(dataPoints: [ BarChartDataPoint(value: 80, xAxisLabel: "4.1", pointLabel: "Four One" , colour: .blue), @@ -47,7 +44,6 @@ import SwiftUI BarChartDataPoint(value: 20, xAxisLabel: "4.3", pointLabel: "Four Three", colour: .purple), BarChartDataPoint(value: 50, xAxisLabel: "4.3", pointLabel: "Four Three", colour: .green)], legendTitle: "Four", - pointStyle: PointStyle(), style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)) ]) diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift index 3475bb41..1041c0c0 100644 --- a/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift +++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift @@ -22,8 +22,7 @@ import SwiftUI BarChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") ], legendTitle: "Data", - pointStyle : PointStyle(), - style : LineStyle()) + style : BarStyle()) ``` # BarChartDataPoint @@ -70,30 +69,25 @@ import SwiftUI - Tag: BarDataSet */ - public struct BarDataSet: SingleDataSet { public let id : UUID public var dataPoints : [BarChartDataPoint] public var legendTitle : String - public var pointStyle : PointStyle public var style : BarStyle /// Initialises a new data set for a Bar 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 drawin. public init(dataPoints : [BarChartDataPoint], legendTitle : String, - pointStyle : PointStyle, style : BarStyle ) { self.id = UUID() self.dataPoints = dataPoints self.legendTitle = legendTitle - self.pointStyle = pointStyle self.style = style } diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift similarity index 100% rename from Sources/SwiftUICharts/BarChart/Models/BarChartStyle.swift rename to Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/BarStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift similarity index 100% rename from Sources/SwiftUICharts/BarChart/Models/BarStyle.swift rename to Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/CornerRadius.swift b/Sources/SwiftUICharts/BarChart/Types/CornerRadius.swift similarity index 100% rename from Sources/SwiftUICharts/BarChart/Models/CornerRadius.swift rename to Sources/SwiftUICharts/BarChart/Types/CornerRadius.swift diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartEnums.swift b/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift similarity index 100% rename from Sources/SwiftUICharts/LineChart/Models/LineChartEnums.swift rename to Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift similarity index 100% rename from Sources/SwiftUICharts/LineChart/Models/LineChartData.swift rename to Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift similarity index 100% rename from Sources/SwiftUICharts/LineChart/Models/MultiLineChartData.swift rename to Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift diff --git a/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift similarity index 98% rename from Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift rename to Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift index 0423283c..8d16b762 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift @@ -85,7 +85,7 @@ import SwiftUI - Tag: LineDataSet */ -public struct LineDataSet: SingleDataSet { +public struct LineDataSet: CTLineChartDataSet { public let id : UUID public var dataPoints : [LineChartDataPoint] diff --git a/Sources/SwiftUICharts/LineChart/Models/MultiLineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift similarity index 100% rename from Sources/SwiftUICharts/LineChart/Models/MultiLineDataSet.swift rename to Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift index 0e350e54..c6d29825 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift @@ -299,3 +299,21 @@ public protocol CTLineChartStyle : CTLineAndBarChartStyle { */ var baseline: Baseline { get set } } + +/** + A protocol to extend functionality of `SingleDataSet` specifically for Line Charts. + + # Reference + [See SingleDataSet](x-source-tag://SingleDataSet) + + - Tag: CTLineChartDataSet + */ +public protocol CTLineChartDataSet: SingleDataSet { + /** + Sets the look of the markers over the data points. + + The markers are layed out when the `ViewModifier` [.pointMarkers](x-source-tag://PointMarkers) + is applied. + */ + var pointStyle : PointStyle { get set } +} diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift similarity index 100% rename from Sources/SwiftUICharts/LineChart/Models/LineChartStyle.swift rename to Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift diff --git a/Sources/SwiftUICharts/LineChart/Models/LineStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift similarity index 100% rename from Sources/SwiftUICharts/LineChart/Models/LineStyle.swift rename to Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift diff --git a/Sources/SwiftUICharts/LineChart/Models/PointStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/PointStyle.swift similarity index 100% rename from Sources/SwiftUICharts/LineChart/Models/PointStyle.swift rename to Sources/SwiftUICharts/LineChart/Models/Style/PointStyle.swift diff --git a/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift index b9b8771a..23ecd61c 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift @@ -7,7 +7,7 @@ import SwiftUI -internal struct Point: Shape where T: SingleDataSet { +internal struct Point: Shape where T: CTLineChartDataSet { private let dataSet : T diff --git a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift index 619454f7..070c1c4e 100644 --- a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift @@ -189,13 +189,7 @@ public protocol SingleDataSet: DataSet { */ var legendTitle : String { get set } - /** - Sets the look of the markers over the data points. - - The markers are layed out when the `ViewModifier` [.pointMarkers](x-source-tag://PointMarkers) - is applied. - */ - var pointStyle : PointStyle { get set } // Line Only ---------------------------- + /** Sets the style for the Data Set (as opposed to Chart Data Style). @@ -262,7 +256,7 @@ public protocol CTChartStyle { Allows for single colour, gradient or gradient with stops control. - - Tag: CTDoughnutChartStyle + - Tag: CTColourStyle */ public protocol CTColourStyle { From f40f2bfceafe6c8cf121ff280aa1dd4f5c44e6b2 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sat, 13 Feb 2021 11:34:13 +0000 Subject: [PATCH 044/152] Remove PointStyle from Pie Chart Data Set. --- .../PieChart/Models/PieChartProtocols.swift | 11 ++--------- .../SwiftUICharts/PieChart/Models/PieDataSet.swift | 3 --- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift index 80baa65b..fd283255 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift @@ -66,16 +66,9 @@ public protocol DoughnutChartDataProtocol : PieAndDoughnutChartDataProtocol wher var chartStyle : CTStyle { get set } } -public protocol CTMultiPieChartDataPoints: CTChartDataPoint { - -} +public protocol CTMultiPieChartDataPoints: CTChartDataPoint {} -public protocol CTMultiPieDataSet: DataSet { - -// var dataSets: - - -} +public protocol CTMultiPieDataSet: DataSet {} // MARK: - Pie and Doughnut extension PieAndDoughnutChartDataProtocol { diff --git a/Sources/SwiftUICharts/PieChart/Models/PieDataSet.swift b/Sources/SwiftUICharts/PieChart/Models/PieDataSet.swift index 2fa465f0..eb24537f 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieDataSet.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieDataSet.swift @@ -12,17 +12,14 @@ public struct PieDataSet: SingleDataSet { public var id : UUID = UUID() public var dataPoints : [PieChartDataPoint] public var legendTitle : String - public var pointStyle : PointStyle public var style : PieSegmentStyle public init(dataPoints : [PieChartDataPoint], legendTitle : String, - pointStyle : PointStyle, style : PieSegmentStyle ) { self.dataPoints = dataPoints self.legendTitle = legendTitle - self.pointStyle = pointStyle self.style = style } From 41cc4955747220522996f9b9fe43e4de255f2389 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sat, 13 Feb 2021 12:02:45 +0000 Subject: [PATCH 045/152] Refactor Protocols - split protocols from extensions, Put into folders. --- .../{ => Protocols}/BarChartProtocols.swift | 10 -- .../BarChartProtocolsExtensions.swift | 19 ++++ .../Models/Protocols/LineChartProtocols.swift | 80 ++++++++++++++ .../LineChartProtocolsExtensions.swift} | 102 +++--------------- .../Shared/Models/LegendData.swift | 5 + .../{ => Protocols}/SharedProtocols.swift | 93 ++++++---------- .../Protocols/SharedProtocolsExtensions.swift | 31 ++++++ .../{ => Protocols}/LineAndBarProtocols.swift | 36 ------- .../LineAndBarProtocolsExtentions.swift | 42 ++++++++ 9 files changed, 225 insertions(+), 193 deletions(-) rename Sources/SwiftUICharts/BarChart/Models/{ => Protocols}/BarChartProtocols.swift (69%) create mode 100644 Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift create mode 100644 Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift rename Sources/SwiftUICharts/LineChart/Models/{LineChartProtocols.swift => Protocols/LineChartProtocolsExtensions.swift} (81%) rename Sources/SwiftUICharts/Shared/Models/{ => Protocols}/SharedProtocols.swift (92%) create mode 100644 Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift rename Sources/SwiftUICharts/SharedLineAndBar/Models/{ => Protocols}/LineAndBarProtocols.swift (78%) create mode 100644 Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift similarity index 69% rename from Sources/SwiftUICharts/BarChart/Models/BarChartProtocols.swift rename to Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index ef713157..20d0e137 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -30,16 +30,6 @@ public protocol BarChartDataProtocol: LineAndBarChartData where CTStyle: CTBarCh } -extension LineAndBarChartData where Self: BarChartDataProtocol { - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let maxValue: Double = self.getMaxValue() - for index in 0...self.chartStyle.yAxisNumberOfLabels { - labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) - } - return labels - } -} // MARK: - Style diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift new file mode 100644 index 00000000..8cc76b36 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift @@ -0,0 +1,19 @@ +// +// BarChartProtocolsExtensions.swift +// +// +// Created by Will Dale on 13/02/2021. +// + +import Foundation + +extension LineAndBarChartData where Self: BarChartDataProtocol { + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let maxValue: Double = self.getMaxValue() + for index in 0...self.chartStyle.yAxisNumberOfLabels { + labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) + } + return labels + } +} diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift new file mode 100644 index 00000000..1217d553 --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -0,0 +1,80 @@ +// +// LineChartProtocols.swift +// +// +// Created by Will Dale on 02/02/2021. +// + +import SwiftUI + +// MARK: - Chart Data +/** + A protocol to extend functionality of `LineAndBarChartData` specifically for Line Charts. + + # Reference + [See LineAndBarChartData](x-source-tag://LineAndBarChartData) + + `LineAndBarChartData` conforms to [ChartData](x-source-tag://ChartData) + + - Tag: LineChartDataProtocol + */ +public protocol LineChartDataProtocol: LineAndBarChartData where CTStyle: CTLineChartStyle { + /** + Data model conatining the style data for the chart. + + # Reference + [CTChartStyle](x-source-tag://CTChartStyle) + */ + var chartStyle : CTStyle { get set } + + /** + Whether it is a normal or filled line. + */ + var isFilled : Bool { get set} + + /** + Returns the position to place the indicator on the line + based on the users touch or pointer input. + + - Parameters: + - rect: Frame of the path. + - dataSet: Dataset used to draw the chart. + - touchLocation: Location of the touch or pointer input. + - Returns: The position to place the indicator. + */ + func getIndicatorLocation(rect: CGRect, dataSet: LineDataSet, touchLocation: CGPoint) -> CGPoint + +} + +// MARK: - Style +/** + A protocol to extend functionality of `CTLineAndBarChartStyle` specifically for Line Charts. + + - Tag: CTLineChartStyle + */ +public protocol CTLineChartStyle : CTLineAndBarChartStyle { + /** + Where to start drawing the line chart from. Zero or data set minium. + + [See Baseline](x-source-tag://Baseline) + */ + var baseline: Baseline { get set } +} + +/** + A protocol to extend functionality of `SingleDataSet` specifically for Line Charts. + + # Reference + [See SingleDataSet](x-source-tag://SingleDataSet) + + - Tag: CTLineChartDataSet + */ +public protocol CTLineChartDataSet: SingleDataSet { + /** + Sets the look of the markers over the data points. + + The markers are layed out when the `ViewModifier` [.pointMarkers](x-source-tag://PointMarkers) + is applied. + */ + var pointStyle : PointStyle { get set } +} diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift similarity index 81% rename from Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift rename to Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index c6d29825..6963f90d 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -1,48 +1,26 @@ // -// LineChartProtocols.swift +// LineChartProtocolsExtensions.swift // // -// Created by Will Dale on 02/02/2021. +// Created by Will Dale on 13/02/2021. // import SwiftUI -/** - A protocol to extend functionality of `LineAndBarChartData` specifically for Line Charts. - - # Reference - [See LineAndBarChartData](x-source-tag://LineAndBarChartData) - - `LineAndBarChartData` conforms to [ChartData](x-source-tag://ChartData) - - - Tag: LineChartDataProtocol - */ -public protocol LineChartDataProtocol: LineAndBarChartData where CTStyle: CTLineChartStyle { - /** - Data model conatining the style data for the chart. - - # Reference - [CTChartStyle](x-source-tag://CTChartStyle) - */ - var chartStyle : CTStyle { get set } - - /** - Whether it is a normal or filled line. - */ - var isFilled : Bool { get set} - - /** - Returns the position to place the indicator on the line - based on the users touch or pointer input. - - - Parameters: - - rect: Frame of the path. - - dataSet: Dataset used to draw the chart. - - touchLocation: Location of the touch or pointer input. - - Returns: The position to place the indicator. - */ - func getIndicatorLocation(rect: CGRect, dataSet: LineDataSet, touchLocation: CGPoint) -> CGPoint - +// MARK: Labels +extension LineAndBarChartData where Self: LineChartDataProtocol { + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let dataRange : Double = self.getRange() + let minValue : Double = self.getMinValue() + let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) + + labels.append(minValue) + for index in 1...self.chartStyle.yAxisNumberOfLabels { + labels.append(minValue + range * Double(index)) + } + return labels + } } // MARK: - Position Indicator @@ -269,51 +247,3 @@ extension LineChartDataProtocol { y: trimmedPoint.boundingRect.midY) } } - -// MARK: Labels -extension LineAndBarChartData where Self: LineChartDataProtocol { - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let dataRange : Double = self.getRange() - let minValue : Double = self.getMinValue() - let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) - - labels.append(minValue) - for index in 1...self.chartStyle.yAxisNumberOfLabels { - labels.append(minValue + range * Double(index)) - } - return labels - } -} -// MARK: - Style -/** - A protocol to extend functionality of `CTLineAndBarChartStyle` specifically for Line Charts. - - - Tag: CTLineChartStyle - */ -public protocol CTLineChartStyle : CTLineAndBarChartStyle { - /** - Where to start drawing the line chart from. Zero or data set minium. - - [See Baseline](x-source-tag://Baseline) - */ - var baseline: Baseline { get set } -} - -/** - A protocol to extend functionality of `SingleDataSet` specifically for Line Charts. - - # Reference - [See SingleDataSet](x-source-tag://SingleDataSet) - - - Tag: CTLineChartDataSet - */ -public protocol CTLineChartDataSet: SingleDataSet { - /** - Sets the look of the markers over the data points. - - The markers are layed out when the `ViewModifier` [.pointMarkers](x-source-tag://PointMarkers) - is applied. - */ - var pointStyle : PointStyle { get set } -} diff --git a/Sources/SwiftUICharts/Shared/Models/LegendData.swift b/Sources/SwiftUICharts/Shared/Models/LegendData.swift index 34d6b27c..d7a59af2 100644 --- a/Sources/SwiftUICharts/Shared/Models/LegendData.swift +++ b/Sources/SwiftUICharts/Shared/Models/LegendData.swift @@ -11,6 +11,8 @@ import SwiftUI /// - Tag: LegendData public struct LegendData: CTColourStyle, Hashable, Identifiable { + // MARK: - Parameters + public var id : UUID /// The type of chart being used. public var chartType : ChartType @@ -29,6 +31,7 @@ public struct LegendData: CTColourStyle, Hashable, Identifiable { public var startPoint : UnitPoint? public var endPoint : UnitPoint? + // MARK: - Single Color /// Legend with single colour /// - Parameters: /// - legend: Text to be displayed @@ -55,6 +58,7 @@ public struct LegendData: CTColourStyle, Hashable, Identifiable { self.colourType = .colour } + // MARK: - Gradient Color /// Legend with a gradient colour /// - Parameters: /// - legend: Text to be displayed @@ -85,6 +89,7 @@ public struct LegendData: CTColourStyle, Hashable, Identifiable { self.colourType = .gradientColour } + // MARK: - Gradient Stops Color /// Legend with a gradient with stop control /// - Parameters: /// - legend: Text to be displayed diff --git a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift similarity index 92% rename from Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift rename to Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 070c1c4e..72702e42 100644 --- a/Sources/SwiftUICharts/Shared/Models/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -113,7 +113,6 @@ public protocol ChartData: ObservableObject, Identifiable { */ func isGreaterThanTwo() -> Bool - // MARK: Touch /** Gets the nearest data points to the touch location. - Parameters: @@ -137,27 +136,6 @@ public protocol ChartData: ObservableObject, Identifiable { func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] } -extension ChartData { - public func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } -} - -extension ChartData where Set: SingleDataSet { - public func isGreaterThanTwo() -> Bool { - return dataSets.dataPoints.count > 2 - } -} -extension ChartData where Set: MultiDataSet { - public func isGreaterThanTwo() -> Bool { - var returnValue: Bool = true - dataSets.dataSets.forEach { dataSet in - returnValue = dataSet.dataPoints.count > 2 - } - return returnValue - } -} - // MARK: - Data Sets /** Main protocol set conformace for types of Data Sets. @@ -211,8 +189,38 @@ public protocol MultiDataSet: DataSet { var dataSets : [DataSet] { get set } } - - +// MARK: - Data Points +/** + Protocol to set base configuration for data points. + + - Tag: CTChartDataPoint + + */ +public protocol CTChartDataPoint: Hashable, Identifiable { + + var id : ID { get } + + /** + Value of the data point + */ + var value : Double { get set } + + /** + A laabel that can be displayed on touch input + + It can eight be displayed in a floating box that tracks the users input location + or placed in the header. [See InfoBoxPlacement](x-source-tag://InfoBoxPlacement). + */ + var pointDescription : String? { get set } + + /** + Date can be used for performing additional calculations. + + [See Calculations](x-source-tag://Calculations) + */ + var date : Date? { get set } + +} // MARK: - Styles /** @@ -288,40 +296,3 @@ public protocol CTColourStyle { /// End point for the gradient var endPoint: UnitPoint? { get set } } - - - - -// MARK: - Data Points - -/** - Protocol to set base configuration for data points. - - - Tag: CTChartDataPoint - - */ -public protocol CTChartDataPoint: Hashable, Identifiable { - - var id : ID { get } - - /** - Value of the data point - */ - var value : Double { get set } - - /** - A laabel that can be displayed on touch input - - It can eight be displayed in a floating box that tracks the users input location - or placed in the header. [See InfoBoxPlacement](x-source-tag://InfoBoxPlacement). - */ - var pointDescription : String? { get set } - - /** - Date can be used for performing additional calculations. - - [See Calculations](x-source-tag://Calculations) - */ - var date : Date? { get set } - -} diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift new file mode 100644 index 00000000..dd3a39c9 --- /dev/null +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift @@ -0,0 +1,31 @@ +// +// SharedProtocolsExtensions.swift +// +// +// Created by Will Dale on 13/02/2021. +// + +import Foundation + +// MARK: Chart Data +extension ChartData { + public func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } +} + +extension ChartData where Set: SingleDataSet { + public func isGreaterThanTwo() -> Bool { + return dataSets.dataPoints.count > 2 + } +} + +extension ChartData where Set: MultiDataSet { + public func isGreaterThanTwo() -> Bool { + var returnValue: Bool = true + dataSets.dataSets.forEach { dataSet in + returnValue = dataSet.dataPoints.count > 2 + } + return returnValue + } +} diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift similarity index 78% rename from Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarProtocols.swift rename to Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index f9028545..b06df499 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -162,39 +162,3 @@ public protocol CTLineAndBarDataPoint: CTChartDataPoint { */ var xAxisLabel : String? { get set } } - - -// MARK: Extensions -extension LineAndBarChartData { - public func getHeaderLocation() -> InfoBoxPlacement { - return self.chartStyle.infoBoxPlacement - } -} -extension LineAndBarChartData where Set: SingleDataSet { - public func getRange() -> Double { - DataFunctions.dataSetRange(from: dataSets) - } - public func getMinValue() -> Double { - DataFunctions.dataSetMinValue(from: dataSets) - } - public func getMaxValue() -> Double { - DataFunctions.dataSetMaxValue(from: dataSets) - } - public func getAverage() -> Double { - DataFunctions.dataSetAverage(from: dataSets) - } -} -extension LineAndBarChartData where Set: MultiDataSet { - public func getRange() -> Double { - DataFunctions.multiDataSetRange(from: dataSets) - } - public func getMinValue() -> Double { - DataFunctions.multiDataSetMinValue(from: dataSets) - } - public func getMaxValue() -> Double { - DataFunctions.multiDataSetMaxValue(from: dataSets) - } - public func getAverage() -> Double { - DataFunctions.multiDataSetAverage(from: dataSets) - } -} diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift new file mode 100644 index 00000000..1453c223 --- /dev/null +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift @@ -0,0 +1,42 @@ +// +// LineAndBarProtocolsExtentions.swift +// +// +// Created by Will Dale on 13/02/2021. +// + +import Foundation + +extension LineAndBarChartData { + public func getHeaderLocation() -> InfoBoxPlacement { + return self.chartStyle.infoBoxPlacement + } +} +extension LineAndBarChartData where Set: SingleDataSet { + public func getRange() -> Double { + DataFunctions.dataSetRange(from: dataSets) + } + public func getMinValue() -> Double { + DataFunctions.dataSetMinValue(from: dataSets) + } + public func getMaxValue() -> Double { + DataFunctions.dataSetMaxValue(from: dataSets) + } + public func getAverage() -> Double { + DataFunctions.dataSetAverage(from: dataSets) + } +} +extension LineAndBarChartData where Set: MultiDataSet { + public func getRange() -> Double { + DataFunctions.multiDataSetRange(from: dataSets) + } + public func getMinValue() -> Double { + DataFunctions.multiDataSetMinValue(from: dataSets) + } + public func getMaxValue() -> Double { + DataFunctions.multiDataSetMaxValue(from: dataSets) + } + public func getAverage() -> Double { + DataFunctions.multiDataSetAverage(from: dataSets) + } +} From dbe21e582a0a8cab904bae35d3806e74962cb23d Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 14 Feb 2021 07:46:59 +0000 Subject: [PATCH 046/152] Tidy up and bug fix for Text. --- .../Models/ChartData/BarChartData.swift | 1 + .../ChartData/GroupedBarChartData.swift | 11 ++++---- .../ChartData/StackedBarChartData.swift | 26 +++++++------------ .../Models/Protocols/BarChartProtocols.swift | 3 --- .../Models/ChartData/LineChartData.swift | 1 + .../Models/ChartData/MultiLineChartData.swift | 1 + .../Shared/ViewModifiers/TouchOverlay.swift | 5 ++-- .../Shared/Views/LegendView.swift | 19 +++++++++++++- 8 files changed, 39 insertions(+), 28 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 0039b2d6..143a2caa 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -220,6 +220,7 @@ public class BarChartData: BarChartDataProtocol { .frame(minWidth: 0, maxWidth: 500) Text(data.xAxisLabel ?? "") .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) Spacer() diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index f69d1730..34fb5e22 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -223,18 +223,19 @@ public class GroupedBarChartData: BarChartDataProtocol { public func getXAxisLabels() -> some View { switch self.chartStyle.xAxisLabelsFrom { case .dataPoint: - HStack(spacing: 100) { + HStack(spacing: self.groupSpacing) { ForEach(dataSets.dataSets) { dataSet in HStack(spacing: 0) { ForEach(dataSet.dataPoints) { data in + Spacer() + .frame(minWidth: 0, maxWidth: 500) Text(data.xAxisLabel ?? "") .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - if data != dataSet.dataPoints[dataSet.dataPoints.count - 1] { - Spacer() - .frame(minWidth: 0, maxWidth: 500) - } + Spacer() + .frame(minWidth: 0, maxWidth: 500) } } } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index eabb6fc7..44ecce30 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -44,26 +44,20 @@ public class StackedBarChartData: BarChartDataProtocol { public func getXAxisLabels() -> some View { switch self.chartStyle.xAxisLabelsFrom { case .dataPoint: - HStack(spacing: 100) { + HStack(spacing: 0) { ForEach(dataSets.dataSets) { dataSet in - HStack(spacing: 0) { - ForEach(dataSet.dataPoints) { data in - Text(data.xAxisLabel ?? "") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) - if data != dataSet.dataPoints[dataSet.dataPoints.count - 1] { - Spacer() - .frame(minWidth: 0, maxWidth: 500) - } - } - } + Spacer() + .frame(minWidth: 0, maxWidth: 500) + Text(dataSet.legendTitle) + .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) + .lineLimit(1) + .minimumScaleFactor(0.5) + 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 diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index 20d0e137..63038379 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -29,9 +29,6 @@ public protocol BarChartDataProtocol: LineAndBarChartData where CTStyle: CTBarCh var chartStyle : CTStyle { get set } } - - - // MARK: - Style /** A protocol to extend functionality of `CTLineAndBarChartStyle` specifically for Bar Charts. diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index cfa90273..1eb2fd88 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -207,6 +207,7 @@ public class LineChartData: LineChartDataProtocol { if let label = data.xAxisLabel { Text(label) .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) } diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index af1242c4..93361aec 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -220,6 +220,7 @@ public class MultiLineChartData: LineChartDataProtocol { if let label = data.xAxisLabel { Text(label) .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index ccb672c7..e9b3e2cb 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -62,15 +62,14 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { self.pointLocations = chartData.getPointLocation(touchLocation: touchLocation, chartSize: geo) + chartData.infoView.touchOverlayInfo = selectedPoints + if chartData.getHeaderLocation() == .floating { setBoxLocationation(boxFrame: boxFrame, chartSize: geo) markerLocation.x = setMarkerXLocation(chartSize: geo) markerLocation.y = setMarkerYLocation(chartSize: geo) - } else if chartData.getHeaderLocation() == .header { - - chartData.infoView.touchOverlayInfo = selectedPoints } } diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index a30f6803..64433111 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -36,7 +36,7 @@ internal struct LegendView: View where T: ChartData { case .bar: bar(legend) - + .if(self.scaleLegend(legend: legend)) { $0.scaleEffect(1.2, anchor: .leading) } case .pie: pie(legend) @@ -45,6 +45,23 @@ internal struct LegendView: View where T: ChartData { } }.id(UUID()) } + private func scaleLegend(legend: LegendData) -> Bool { + + + var matched : Bool = false + + chartData.infoView.touchOverlayInfo.forEach { (dataPoint) in + if matched { return } + + if legend.id == dataPoint.id as! UUID { + matched = true + } + } + + return matched + + } + func line(_ legend: LegendData) -> some View { Group { From 21ecc06a32a4c7efc0c6e537158b99005f30ab68 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 14 Feb 2021 07:47:22 +0000 Subject: [PATCH 047/152] Add colours to stacked bar chart. --- .../BarChart/Views/StackedBarChart.swift | 66 ++++-- ...etSubView.swift => BarChartSubViews.swift} | 2 +- .../BarChart/Views/SubViews/Bars.swift | 204 ++++++++++++------ 3 files changed, 195 insertions(+), 77 deletions(-) rename Sources/SwiftUICharts/BarChart/Views/SubViews/{BarChartDataSetSubView.swift => BarChartSubViews.swift} (98%) diff --git a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift index c50612fe..c1c281e2 100644 --- a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift @@ -16,6 +16,8 @@ public struct StackedBarChart: View where ChartData: StackedBarChartD self.chartData = chartData } + @State private var startAnimation : Bool = false + public var body: some View { if chartData.isGreaterThanTwo() { @@ -23,9 +25,15 @@ public struct StackedBarChart: View where ChartData: StackedBarChartD HStack(alignment: .bottom, spacing: 0) { ForEach(chartData.dataSets.dataSets) { dataSet in - MultiPartBarView(dataSet: dataSet) - .scaleEffect(y: CGFloat(DataFunctions.dataSetMaxValue(from: dataSet) / chartData.getMaxValue()), - anchor: .bottom) + MultiPartBarSubView(dataSet: dataSet) + .scaleEffect(y: startAnimation ? CGFloat(DataFunctions.dataSetMaxValue(from: dataSet) / chartData.getMaxValue()) : 0, anchor: .bottom) + .scaleEffect(x: dataSet.style.barWidth, anchor: .center) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = false + } } } @@ -33,31 +41,61 @@ public struct StackedBarChart: View where ChartData: StackedBarChartD } } -struct MultiPartBarView: View { +/** + + */ +internal struct MultiPartBarSubView: View { - let dataSet : BarDataSet + private let dataSet : BarDataSet - init(dataSet: BarDataSet) { + internal init(dataSet: BarDataSet) { self.dataSet = dataSet } - var body: some View { + internal var body: some View { GeometryReader { geo in VStack(spacing: 0) { - ForEach(dataSet.dataPoints.reversed()) { datapoint in + ForEach(dataSet.dataPoints.reversed()) { dataPoint in + + if dataPoint.colourType == .colour, + let colour = dataPoint.colour + { + + ColourPartBar(colour, getHeight(height : geo.size.height, + dataSet : dataSet, + dataPoint : dataPoint)) + + } else if dataPoint.colourType == .gradientColour, + let colours = dataPoint.colours, + let startPoint = dataPoint.startPoint, + let endPoint = dataPoint.endPoint + { + + GradientColoursPartBar(colours, startPoint, endPoint, getHeight(height: geo.size.height, + dataSet : dataSet, + dataPoint : dataPoint)) + + } else if dataPoint.colourType == .gradientStops, + let stops = dataPoint.stops, + let startPoint = dataPoint.startPoint, + let endPoint = dataPoint.endPoint + { + + let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) + + GradientStopsPartBar(safeStops, startPoint, endPoint, getHeight(height: geo.size.height, + dataSet : dataSet, + dataPoint : dataPoint)) + } - Rectangle() - .fill(datapoint.colour ?? .pink) - .frame(height: getHeight(height: geo.size.height, - dataSet: dataSet, - dataPoint: datapoint)) } } } } - func getHeight(height: CGFloat, dataSet: BarDataSet, dataPoint: BarChartDataPoint) -> CGFloat { + + private func getHeight(height: CGFloat, dataSet: BarDataSet, dataPoint: BarChartDataPoint) -> CGFloat { let value = dataPoint.value let sum = dataSet.dataPoints.reduce(0) { $0 + $1.value } return height * CGFloat(value / sum) diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartDataSetSubView.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift similarity index 98% rename from Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartDataSetSubView.swift rename to Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift index aa5e97ce..26b44429 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartDataSetSubView.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift @@ -1,5 +1,5 @@ // -// BarChartDataSetSubView.swift +// BarChartSubViews.swift // // // Created by Will Dale on 26/01/2021. diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift index d7a3b4a8..1167def8 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift @@ -7,19 +7,20 @@ import SwiftUI -struct ColourBar: View { - - let colour : Color - let data : BarChartDataPoint - let maxValue : Double - let chartStyle : BarChartStyle - let style : BarStyle - - init(_ colour : Color, - _ data : BarChartDataPoint, - _ maxValue : Double, - _ chartStyle : BarChartStyle, - _ style : BarStyle +// MARK: Standard +internal struct ColourBar: View { + + private let colour : Color + private let data : BarChartDataPoint + private let maxValue : Double + private let chartStyle : BarChartStyle + private let style : BarStyle + + internal init(_ colour : Color, + _ data : BarChartDataPoint, + _ maxValue : Double, + _ chartStyle : BarChartStyle, + _ style : BarStyle ) { self.colour = colour self.data = data @@ -27,43 +28,43 @@ struct ColourBar: View { 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) + + @State private var startAnimation : Bool = false + + internal 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 } - .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 : BarChartDataPoint - let maxValue : Double - let chartStyle : BarChartStyle - let style : BarStyle - - init(_ colours : [Color], - _ startPoint : UnitPoint, - _ endPoint : UnitPoint, - _ data : BarChartDataPoint, - _ maxValue : Double, - _ chartStyle : BarChartStyle, - _ style : BarStyle +internal struct GradientColoursBar: View { + + private let colours : [Color] + private let startPoint : UnitPoint + private let endPoint : UnitPoint + private let data : BarChartDataPoint + private let maxValue : Double + private let chartStyle : BarChartStyle + private let style : BarStyle + + internal init(_ colours : [Color], + _ startPoint : UnitPoint, + _ endPoint : UnitPoint, + _ data : BarChartDataPoint, + _ maxValue : Double, + _ chartStyle : BarChartStyle, + _ style : BarStyle ) { self.colours = colours self.startPoint = startPoint @@ -73,11 +74,14 @@ struct GradientColoursBar: View { 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) + @State private var startAnimation : Bool = false + + internal 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)) @@ -92,23 +96,23 @@ struct GradientColoursBar: View { } } -struct GradientStopsBar: View { - - let stops : [Gradient.Stop] - let startPoint : UnitPoint - let endPoint : UnitPoint - let data : BarChartDataPoint - let maxValue : Double - let chartStyle : BarChartStyle - let style : BarStyle - - init(_ stops : [Gradient.Stop], - _ startPoint : UnitPoint, - _ endPoint : UnitPoint, - _ data : BarChartDataPoint, - _ maxValue : Double, - _ chartStyle : BarChartStyle, - _ style : BarStyle +internal struct GradientStopsBar: View { + + private let stops : [Gradient.Stop] + private let startPoint : UnitPoint + private let endPoint : UnitPoint + private let data : BarChartDataPoint + private let maxValue : Double + private let chartStyle : BarChartStyle + private let style : BarStyle + + internal init(_ stops : [Gradient.Stop], + _ startPoint : UnitPoint, + _ endPoint : UnitPoint, + _ data : BarChartDataPoint, + _ maxValue : Double, + _ chartStyle : BarChartStyle, + _ style : BarStyle ) { self.stops = stops self.startPoint = startPoint @@ -119,10 +123,13 @@ struct GradientStopsBar: View { self.style = style } - @State var startAnimation : Bool = false + @State private 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) + internal 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)) @@ -137,4 +144,77 @@ struct GradientStopsBar: View { } } +// MARK: - Multi Part +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) + } +} + +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) + } +} + +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) + } +} From a0f0c9234d4440ca67a2625ceead81d5c778339d Mon Sep 17 00:00:00 2001 From: Will Dale Date: Mon, 15 Feb 2021 10:48:21 +0000 Subject: [PATCH 048/152] Restructure GroupedBarChart data. --- .../BarChart/Models/BarChartDataPoint.swift | 82 ++++++- .../Models/ChartData/BarChartData.swift | 153 +++++++------- .../ChartData/GroupedBarChartData.swift | 113 +++------- .../ChartData/StackedBarChartData.swift | 167 ++++++++------- .../BarChart/Models/DataSet/BarDataSet.swift | 12 +- .../Models/DataSet/MultiBarDataSet.swift | 118 ----------- .../Models/DataSet/MultiBarDataSets.swift | 41 ++++ .../Models/Protocols/BarChartProtocols.swift | 56 +++++ .../BarChart/Types/CornerRadius.swift | 2 +- .../BarChart/Views/BarChart.swift | 16 +- .../BarChart/Views/GroupedBarChart.swift | 42 ++-- .../BarChart/Views/StackedBarChart.swift | 200 +++++++++--------- .../Views/SubViews/BarChartSubViews.swift | 54 +++-- .../BarChart/Views/SubViews/Bars.swift | 86 ++++---- .../Models/Protocols/LineChartProtocols.swift | 7 +- .../Models/Protocols/SharedProtocols.swift | 9 +- .../Shared/Views/LegendView.swift | 8 +- 17 files changed, 591 insertions(+), 575 deletions(-) delete mode 100644 Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSet.swift create mode 100644 Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift index 9e5a5f6d..ca76bb96 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift @@ -62,7 +62,7 @@ import SwiftUI - Tag: BarChartDataPoint */ -public struct BarChartDataPoint: CTLineAndBarDataPoint, CTColourStyle { +public struct BarChartDataPoint: CTStandardBarDataPoint { public let id = UUID() @@ -133,7 +133,6 @@ public struct BarChartDataPoint: CTLineAndBarDataPoint, CTColourStyle { self.colours = colours self.startPoint = startPoint self.endPoint = endPoint - self.colourType = .gradientColour } @@ -168,3 +167,82 @@ public struct BarChartDataPoint: CTLineAndBarDataPoint, CTColourStyle { } } + +// MARK: - Grouped +public struct GroupedBarChartDataPoint: CTGroupedBarDataPoint { + + public let id = UUID() + + public var value : Double + public var xAxisLabel : String? + public var pointDescription : String? + public var date : Date? + + public var colourType : ColourType + public var colour : Color? + public var colours : [Color]? + public var stops : [GradientStop]? + public var startPoint : UnitPoint? + public var endPoint : UnitPoint? + + 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 + } + + 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 + } + + 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/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 143a2caa..403f206e 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -174,6 +174,7 @@ public class BarChartData: BarChartDataProtocol { @Published public var dataSets : BarDataSet @Published public var metadata : ChartMetadata @Published public var xAxisLabels : [String]? + @Published public var barStyle : BarStyle @Published public var chartStyle : BarChartStyle @Published public var legends : [LegendData] @Published public var viewData : ChartViewData @@ -194,12 +195,14 @@ public class BarChartData: BarChartDataProtocol { 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]() @@ -274,81 +277,81 @@ public class BarChartData: BarChartDataProtocol { // MARK: - Legends public func setupLegends() { - switch self.dataSets.style.colourFrom { - case .barStyle: - if dataSets.style.colourType == .colour, - let colour = dataSets.style.colour - { - self.legends.append(LegendData(id : dataSets.id, - legend : dataSets.legendTitle, - colour : colour, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if dataSets.style.colourType == .gradientColour, - let colours = dataSets.style.colours - { - self.legends.append(LegendData(id : dataSets.id, - legend : dataSets.legendTitle, - colours : colours, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if dataSets.style.colourType == .gradientStops, - let stops = dataSets.style.stops - { - self.legends.append(LegendData(id : dataSets.id, - legend : dataSets.legendTitle, - stops : stops, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } - case .dataPoints: - - for data in dataSets.dataPoints { - - if data.colourType == .colour, - let colour = data.colour, - let legend = data.pointDescription - { - self.legends.append(LegendData(id : data.id, - legend : legend, - colour : colour, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if data.colourType == .gradientColour, - let colours = data.colours, - let legend = data.pointDescription - { - self.legends.append(LegendData(id : data.id, - legend : legend, - colours : colours, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if data.colourType == .gradientStops, - let stops = data.stops, - let legend = data.pointDescription - { - self.legends.append(LegendData(id : data.id, - legend : legend, - stops : stops, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } - } - } +// switch self.dataSets.style.colourFrom { +// case .barStyle: +// if dataSets.style.colourType == .colour, +// let colour = dataSets.style.colour +// { +// self.legends.append(LegendData(id : dataSets.id, +// legend : dataSets.legendTitle, +// colour : colour, +// strokeStyle: nil, +// prioity : 1, +// chartType : .bar)) +// } else if dataSets.style.colourType == .gradientColour, +// let colours = dataSets.style.colours +// { +// self.legends.append(LegendData(id : dataSets.id, +// legend : dataSets.legendTitle, +// colours : colours, +// startPoint : .leading, +// endPoint : .trailing, +// strokeStyle: nil, +// prioity : 1, +// chartType : .bar)) +// } else if dataSets.style.colourType == .gradientStops, +// let stops = dataSets.style.stops +// { +// self.legends.append(LegendData(id : dataSets.id, +// legend : dataSets.legendTitle, +// stops : stops, +// startPoint : .leading, +// endPoint : .trailing, +// strokeStyle: nil, +// prioity : 1, +// chartType : .bar)) +// } +// case .dataPoints: +// +// for data in dataSets.dataPoints { +// +// if data.colourType == .colour, +// let colour = data.colour, +// let legend = data.pointDescription +// { +// self.legends.append(LegendData(id : data.id, +// legend : legend, +// colour : colour, +// strokeStyle: nil, +// prioity : 1, +// chartType : .bar)) +// } else if data.colourType == .gradientColour, +// let colours = data.colours, +// let legend = data.pointDescription +// { +// self.legends.append(LegendData(id : data.id, +// legend : legend, +// colours : colours, +// startPoint : .leading, +// endPoint : .trailing, +// strokeStyle: nil, +// prioity : 1, +// chartType : .bar)) +// } else if data.colourType == .gradientStops, +// let stops = data.stops, +// let legend = data.pointDescription +// { +// self.legends.append(LegendData(id : data.id, +// legend : legend, +// stops : stops, +// startPoint : .leading, +// endPoint : .trailing, +// strokeStyle: nil, +// prioity : 1, +// chartType : .bar)) +// } +// } +// } } public typealias Set = BarDataSet diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 34fb5e22..113cca3c 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -176,17 +176,20 @@ import SwiftUI - Tag: GroupedBarChartData */ public class GroupedBarChartData: BarChartDataProtocol { + // MARK: - Properties public let id : UUID = UUID() - @Published public var dataSets : MultiBarDataSet + @Published public var dataSets : GroupedBarDataSets @Published public var metadata : ChartMetadata @Published public var xAxisLabels : [String]? + @Published public var barStyle : BarStyle @Published public var chartStyle : BarChartStyle @Published public var legends : [LegendData] @Published public var viewData : ChartViewData - @Published public var infoView : InfoViewData = InfoViewData() + @Published public var infoView : InfoViewData = InfoViewData() + public var groupLegends : [GroupedBarLegend] public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) @@ -201,16 +204,20 @@ public class GroupedBarChartData: BarChartDataProtocol { /// - 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 : MultiBarDataSet, + public init(dataSets : GroupedBarDataSets, metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, + barStyle : BarStyle = BarStyle(), chartStyle : BarChartStyle = BarChartStyle(), + groupLegends: [GroupedBarLegend], noDataText : Text = Text("No Data") ) { self.dataSets = dataSets self.metadata = metadata self.xAxisLabels = xAxisLabels + self.barStyle = barStyle self.chartStyle = chartStyle + self.groupLegends = groupLegends self.noDataText = noDataText self.legends = [LegendData]() self.viewData = ChartViewData() @@ -263,9 +270,9 @@ public class GroupedBarChartData: BarChartDataProtocol { } // MARK: - Touch - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [GroupedBarChartDataPoint] { - var points : [BarChartDataPoint] = [] + var points : [GroupedBarChartDataPoint] = [] // Divide the chart into equal sections. let superXSection : CGFloat = (chartSize.size.width / CGFloat(dataSets.dataSets.count)) @@ -296,85 +303,29 @@ public class GroupedBarChartData: BarChartDataProtocol { // MARK: - Legends public func setupLegends() { - switch dataSets.dataSets[0].style.colourFrom { - case .barStyle: - if dataSets.dataSets[0].style.colourType == .colour, - let colour = dataSets.dataSets[0].style.colour - { - self.legends.append(LegendData(id : dataSets.dataSets[0].id, - legend : dataSets.dataSets[0].legendTitle, - colour : colour, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if dataSets.dataSets[0].style.colourType == .gradientColour, - let colours = dataSets.dataSets[0].style.colours - { - self.legends.append(LegendData(id : dataSets.dataSets[0].id, - legend : dataSets.dataSets[0].legendTitle, - colours : colours, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if dataSets.dataSets[0].style.colourType == .gradientStops, - let stops = dataSets.dataSets[0].style.stops - { - self.legends.append(LegendData(id : dataSets.dataSets[0].id, - legend : dataSets.dataSets[0].legendTitle, - stops : stops, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } - case .dataPoints: - - for data in dataSets.dataSets[0].dataPoints { - - if data.colourType == .colour, - let colour = data.colour, - let legend = data.pointDescription - { - self.legends.append(LegendData(id : data.id, - legend : legend, - colour : colour, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if data.colourType == .gradientColour, - let colours = data.colours, - let legend = data.pointDescription - { - self.legends.append(LegendData(id : data.id, - legend : legend, - colours : colours, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if data.colourType == .gradientStops, - let stops = data.stops, - let legend = data.pointDescription - { - self.legends.append(LegendData(id : data.id, - legend : legend, - stops : stops, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } - } + + for legend in self.groupLegends { + self.legends.append(LegendData(id: UUID(), + legend: legend.title, + colour: legend.colour, + strokeStyle: nil, + prioity: 1, + chartType: .bar)) } } - public typealias Set = MultiBarDataSet - public typealias DataPoint = BarChartDataPoint + public typealias Set = GroupedBarDataSets + public typealias DataPoint = GroupedBarChartDataPoint public typealias CTStyle = BarChartStyle } +public struct GroupedBarLegend { + + public let title : String + public let colour: Color + + public init(title: String, colour: Color) { + self.title = title + self.colour = colour + } +} diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index 44ecce30..c43f168f 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -12,26 +12,29 @@ public class StackedBarChartData: BarChartDataProtocol { // MARK: - Properties public let id : UUID = UUID() - @Published public var dataSets : MultiBarDataSet + @Published public var dataSets : GroupedBarDataSets @Published public var metadata : ChartMetadata @Published public var xAxisLabels : [String]? + @Published public var barStyle : BarStyle @Published public var chartStyle : BarChartStyle @Published public var legends : [LegendData] @Published public var viewData : ChartViewData - @Published public var infoView : InfoViewData = InfoViewData() + @Published public var infoView : InfoViewData = InfoViewData() public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) - public init(dataSets : MultiBarDataSet, + public init(dataSets : GroupedBarDataSets, 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]() @@ -77,9 +80,9 @@ public class StackedBarChartData: BarChartDataProtocol { } // MARK: - Touch - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [GroupedBarChartDataPoint] { - var points : [BarChartDataPoint] = [] + var points : [GroupedBarChartDataPoint] = [] // Filter to get the right dataset based on the x axis. let superXSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataSets.count) @@ -130,83 +133,83 @@ public class StackedBarChartData: BarChartDataProtocol { // MARK: - Legends public func setupLegends() { - switch dataSets.dataSets[0].style.colourFrom { - case .barStyle: - if dataSets.dataSets[0].style.colourType == .colour, - let colour = dataSets.dataSets[0].style.colour - { - self.legends.append(LegendData(id : dataSets.dataSets[0].id, - legend : dataSets.dataSets[0].legendTitle, - colour : colour, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if dataSets.dataSets[0].style.colourType == .gradientColour, - let colours = dataSets.dataSets[0].style.colours - { - self.legends.append(LegendData(id : dataSets.dataSets[0].id, - legend : dataSets.dataSets[0].legendTitle, - colours : colours, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if dataSets.dataSets[0].style.colourType == .gradientStops, - let stops = dataSets.dataSets[0].style.stops - { - self.legends.append(LegendData(id : dataSets.dataSets[0].id, - legend : dataSets.dataSets[0].legendTitle, - stops : stops, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } - case .dataPoints: - - for data in dataSets.dataSets[0].dataPoints { - - if data.colourType == .colour, - let colour = data.colour, - let legend = data.pointDescription - { - self.legends.append(LegendData(id : data.id, - legend : legend, - colour : colour, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if data.colourType == .gradientColour, - let colours = data.colours, - let legend = data.pointDescription - { - self.legends.append(LegendData(id : data.id, - legend : legend, - colours : colours, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if data.colourType == .gradientStops, - let stops = data.stops, - let legend = data.pointDescription - { - self.legends.append(LegendData(id : data.id, - legend : legend, - stops : stops, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } - } - } +// switch dataSets.dataSets[0].style.colourFrom { +// case .barStyle: +// if dataSets.dataSets[0].style.colourType == .colour, +// let colour = dataSets.dataSets[0].style.colour +// { +// self.legends.append(LegendData(id : dataSets.dataSets[0].id, +// legend : dataSets.dataSets[0].legendTitle, +// colour : colour, +// strokeStyle: nil, +// prioity : 1, +// chartType : .bar)) +// } else if dataSets.dataSets[0].style.colourType == .gradientColour, +// let colours = dataSets.dataSets[0].style.colours +// { +// self.legends.append(LegendData(id : dataSets.dataSets[0].id, +// legend : dataSets.dataSets[0].legendTitle, +// colours : colours, +// startPoint : .leading, +// endPoint : .trailing, +// strokeStyle: nil, +// prioity : 1, +// chartType : .bar)) +// } else if dataSets.dataSets[0].style.colourType == .gradientStops, +// let stops = dataSets.dataSets[0].style.stops +// { +// self.legends.append(LegendData(id : dataSets.dataSets[0].id, +// legend : dataSets.dataSets[0].legendTitle, +// stops : stops, +// startPoint : .leading, +// endPoint : .trailing, +// strokeStyle: nil, +// prioity : 1, +// chartType : .bar)) +// } +// case .dataPoints: +// +// for data in dataSets.dataSets[0].dataPoints { +// +// if data.colourType == .colour, +// let colour = data.colour, +// let legend = data.pointDescription +// { +// self.legends.append(LegendData(id : data.id, +// legend : legend, +// colour : colour, +// strokeStyle: nil, +// prioity : 1, +// chartType : .bar)) +// } else if data.colourType == .gradientColour, +// let colours = data.colours, +// let legend = data.pointDescription +// { +// self.legends.append(LegendData(id : data.id, +// legend : legend, +// colours : colours, +// startPoint : .leading, +// endPoint : .trailing, +// strokeStyle: nil, +// prioity : 1, +// chartType : .bar)) +// } else if data.colourType == .gradientStops, +// let stops = data.stops, +// let legend = data.pointDescription +// { +// self.legends.append(LegendData(id : data.id, +// legend : legend, +// stops : stops, +// startPoint : .leading, +// endPoint : .trailing, +// strokeStyle: nil, +// prioity : 1, +// chartType : .bar)) +// } +// } +// } } - public typealias Set = MultiBarDataSet - public typealias DataPoint = BarChartDataPoint + public typealias Set = GroupedBarDataSets + public typealias DataPoint = GroupedBarChartDataPoint public typealias CTStyle = BarChartStyle } diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift index 1041c0c0..a1bedb6f 100644 --- a/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift +++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift @@ -69,28 +69,24 @@ import SwiftUI - Tag: BarDataSet */ -public struct BarDataSet: SingleDataSet { +public struct BarDataSet: CTStandardBarChartDataSet { public let id : UUID public var dataPoints : [BarChartDataPoint] public var legendTitle : String - public var style : BarStyle /// Initialises a new data set for a Bar Chart. /// - Parameters: /// - dataPoints: Array of elements. /// - legendTitle: label for the data in legend. - /// - style: Styling for how the line will be drawin. public init(dataPoints : [BarChartDataPoint], - legendTitle : String, - style : BarStyle + legendTitle : String ) { self.id = UUID() self.dataPoints = dataPoints self.legendTitle = legendTitle - self.style = style } - public typealias ID = UUID - public typealias Styling = BarStyle + public typealias ID = UUID + public typealias DataPoint = BarChartDataPoint } diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSet.swift deleted file mode 100644 index 77fff733..00000000 --- a/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSet.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// MultiBarDataSet.swift -// -// -// Created by Will Dale on 04/02/2021. -// - -import SwiftUI - -/** - Data set for a multi bar, bar charts. - - Contains information about each of bar sets within the chart. - - # Example - ``` - let data = MultiBarDataSet(dataSets: [ - BarDataSet(dataPoints: [ - BarChartDataPoint(value: 10, xAxisLabel: "1.1", pointLabel: "One One" , colour: .blue), - BarChartDataPoint(value: 20, xAxisLabel: "1.2", pointLabel: "One Two" , colour: .yellow), - BarChartDataPoint(value: 30, xAxisLabel: "1.3", pointLabel: "One Three", colour: .purple), - BarChartDataPoint(value: 40, xAxisLabel: "1.4", pointLabel: "One Four" , colour: .green)], - legendTitle: "One", - pointStyle: PointStyle(), - style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)), - BarDataSet(dataPoints: [ - BarChartDataPoint(value: 50, xAxisLabel: "2.1", pointLabel: "Two One" , colour: .blue), - BarChartDataPoint(value: 10, xAxisLabel: "2.2", pointLabel: "Two Two" , colour: .yellow), - BarChartDataPoint(value: 40, xAxisLabel: "2.3", pointLabel: "Two Three", colour: .purple), - BarChartDataPoint(value: 60, xAxisLabel: "2.3", pointLabel: "Two Three", colour: .green)], - legendTitle: "Two", - pointStyle: PointStyle(), - style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)), - BarDataSet(dataPoints: [ - BarChartDataPoint(value: 10, xAxisLabel: "3.1", pointLabel: "Three One" , colour: .blue), - BarChartDataPoint(value: 50, xAxisLabel: "3.2", pointLabel: "Three Two" , colour: .yellow), - BarChartDataPoint(value: 30, xAxisLabel: "3.3", pointLabel: "Three Three", colour: .purple), - BarChartDataPoint(value: 99, xAxisLabel: "3.4", pointLabel: "Three Four" , colour: .green)], - legendTitle: "Three", - pointStyle: PointStyle(), - style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)), - BarDataSet(dataPoints: [ - BarChartDataPoint(value: 80, xAxisLabel: "4.1", pointLabel: "Four One" , colour: .blue), - BarChartDataPoint(value: 10, xAxisLabel: "4.2", pointLabel: "Four Two" , colour: .yellow), - BarChartDataPoint(value: 20, xAxisLabel: "4.3", pointLabel: "Four Three", colour: .purple), - BarChartDataPoint(value: 50, xAxisLabel: "4.3", pointLabel: "Four Three", colour: .green)], - legendTitle: "Four", - style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)) - ]) - ``` - - # DataSet - ``` - BarDataSet(dataPoints: [BarChartDataPoint], - legendTitle: String, - style: BarStyle) - ``` - - - # BarChartDataPoint - ``` - BarChartDataPoint(value: Double, - xAxisLabel: String?, - pointLabel: String?, - date: Date?) - ``` - - # BarStyle - ``` - BarStyle(barWidth : CGFloat, - cornerRadius : CornerRadius, - colourFrom : ColourFrom, - ...) - - BarStyle(... - colour: Color) - - BarStyle(... - colours: [Color], - startPoint: UnitPoint, - endPoint: UnitPoint) - - BarStyle(... - stops: [GradientStop], - startPoint: UnitPoint, - endPoint: UnitPoint) - ``` - - --- - # Also See - - [BarDataSet](x-source-tag://BarDataSet) - - [BarChartDataPoint](x-source-tag://BarChartDataPoint) - - [BarStyle](x-source-tag://BarStyle) - - [CornerRadius](x-source-tag://CornerRadius) - - [ColourFrom](x-source-tag://ColourFrom) - - [GradientStop](x-source-tag://GradientStop) - - # Conforms to - - MultiDataSet - - DataSet - - Hashable - - Identifiable - - - - Tag: MultiBarDataSet - */ -public struct MultiBarDataSet: MultiDataSet { - - public let id : UUID - public var dataSets : [BarDataSet] - - /// Initialises a new data set for Multiline Line Chart. - public init(dataSets: [BarDataSet]) { - self.id = UUID() - self.dataSets = dataSets - } -} - diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift new file mode 100644 index 00000000..eabf0e90 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift @@ -0,0 +1,41 @@ +// +// MultiBarDataSet.swift +// +// +// Created by Will Dale on 04/02/2021. +// + +import SwiftUI + +public struct GroupedBarDataSets: MultiDataSet { + + public let id : UUID + public var dataSets : [GroupedBarDataSet] + + /// Initialises a new data set for Multiline Line Chart. + public init(dataSets: [GroupedBarDataSet]) { + self.id = UUID() + self.dataSets = dataSets + } +} + + +public struct GroupedBarDataSet: CTGroupedBarChartDataSet { + + public let id : UUID + public var dataPoints : [GroupedBarChartDataPoint] + public var legendTitle : String + + /// Initialises a new data set for a Bar Chart. + public init(dataPoints : [GroupedBarChartDataPoint], + legendTitle : String + ) { + self.id = UUID() + self.dataPoints = dataPoints + self.legendTitle = legendTitle + } + + public typealias ID = UUID + public typealias DataPoint = GroupedBarChartDataPoint + public typealias Styling = BarStyle +} diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index 63038379..b1454a6d 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -20,6 +20,7 @@ import SwiftUI - Tag: BarChartDataProtocol */ public protocol BarChartDataProtocol: LineAndBarChartData where CTStyle: CTBarChartStyle { + var barStyle : BarStyle { get set } /** Data model conatining the style data for the chart. @@ -29,6 +30,11 @@ public protocol BarChartDataProtocol: LineAndBarChartData where CTStyle: CTBarCh var chartStyle : CTStyle { get set } } +//public protocol GroupedBarChartDataProtocol: BarChartDataProtocol {} + + + + // MARK: - Style /** A protocol to extend functionality of `CTLineAndBarChartStyle` specifically for Bar Charts. @@ -38,3 +44,53 @@ public protocol BarChartDataProtocol: LineAndBarChartData where CTStyle: CTBarCh - Tag: CTBarChartStyle */ public protocol CTBarChartStyle: CTLineAndBarChartStyle {} + + + + + + + +// MARK: - DataSet +/** + A protocol to extend functionality of `SingleDataSet` specifically for Standard Bar Charts. + + # Reference + [See SingleDataSet](x-source-tag://SingleDataSet) + + - Tag: CTBarChartDataSet + */ +public protocol CTStandardBarChartDataSet: SingleDataSet {} + +public protocol CTGroupedBarChartDataSet: SingleDataSet {} + +public protocol CTSStackedBarChartDataSet: SingleDataSet {} + + + + + + + + +// MARK: - DataPoints +/** + A protocol to extend functionality of `CTLineAndBarDataPoint` specifically for standard Bar Charts. + + - Tag: CTStandardBarDataPoint + */ +public protocol CTBarDataPoint: CTLineAndBarDataPoint {} + +/** + A protocol to extend functionality of `CTLineAndBarDataPoint` specifically for standard Bar Charts. + + - Tag: CTStandardBarDataPoint + */ +public protocol CTStandardBarDataPoint: CTBarDataPoint, CTColourStyle {} +/** + A protocol to extend functionality of `CTLineAndBarDataPoint` specifically for multi part Bar Charts. + i.e: Grouped or Stacked + + - Tag: CTMultiPartBarDataPoint + */ +public protocol CTGroupedBarDataPoint: CTBarDataPoint, CTColourStyle {} diff --git a/Sources/SwiftUICharts/BarChart/Types/CornerRadius.swift b/Sources/SwiftUICharts/BarChart/Types/CornerRadius.swift index 8975e21c..6e25f959 100644 --- a/Sources/SwiftUICharts/BarChart/Types/CornerRadius.swift +++ b/Sources/SwiftUICharts/BarChart/Types/CornerRadius.swift @@ -23,7 +23,7 @@ public struct CornerRadius: Hashable { var bottom : CGFloat /// Set the coner radius for the bar shapes - public init(top: CGFloat, bottom: CGFloat) { + public init(top: CGFloat = 15.0, bottom: CGFloat = 0.0) { self.top = top self.bottom = bottom } diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift index 7e7c6221..f48509b5 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift @@ -20,22 +20,16 @@ public struct BarChart: View where ChartData: BarChartData { HStack(spacing: 0) { ForEach(chartData.dataSets.dataPoints) { dataPoint in - switch chartData.dataSets.style.colourFrom { + switch chartData.barStyle.colourFrom { case .barStyle: - BarChartDataSetSubView(colourType: chartData.dataSets.style.colourType, - dataPoint: dataPoint, - style: chartData.dataSets.style, - chartStyle: chartData.chartStyle, - maxValue: chartData.getMaxValue()) + BarChartDataSetSubView(chartData : chartData, + dataPoint : dataPoint) case .dataPoints: - BarChartDataPointSubView(colourType : dataPoint.colourType, - dataPoint : dataPoint, - style : chartData.dataSets.style, - chartStyle : chartData.chartStyle, - maxValue : chartData.getMaxValue()) + BarChartDataPointSubView(chartData : chartData, + dataPoint : dataPoint) } } diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index 9e29a77f..19833403 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -13,13 +13,14 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD private let groupSpacing : CGFloat - public init(chartData: ChartData, groupSpacing : CGFloat) { + 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) { @@ -27,23 +28,30 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD HStack(spacing: 0) { ForEach(dataSet.dataPoints) { dataPoint in - switch dataSet.style.colourFrom { - case .barStyle: - - BarChartDataSetSubView(colourType: dataSet.style.colourType, - dataPoint: dataPoint, - style: dataSet.style, - chartStyle: chartData.chartStyle, - maxValue: chartData.getMaxValue()) + if dataPoint.colourType == .colour, + let colour = dataPoint.colour + { - case .dataPoints: - - BarChartDataPointSubView(colourType: dataPoint.colourType, - dataPoint: dataPoint, - style: dataSet.style, - chartStyle: chartData.chartStyle, - maxValue: chartData.getMaxValue()) + ColourBar(colour, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + } else if dataPoint.colourType == .gradientColour, + let colours = dataPoint.colours, + let startPoint = dataPoint.startPoint, + let endPoint = dataPoint.endPoint + { + + GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + + } else if dataPoint.colourType == .gradientStops, + let stops = dataPoint.stops, + let startPoint = dataPoint.startPoint, + let endPoint = dataPoint.endPoint + { + + let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) + + GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + } } } diff --git a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift index c1c281e2..0a1d030c 100644 --- a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift @@ -1,103 +1,103 @@ +//// +//// StackedBarChart.swift +//// +//// +//// Created by Will Dale on 12/02/2021. +//// // -// StackedBarChart.swift -// +//import SwiftUI // -// Created by Will Dale on 12/02/2021. // - -import SwiftUI - - -public struct StackedBarChart: View where ChartData: StackedBarChartData { - - @ObservedObject var chartData: ChartData - - 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 - - MultiPartBarSubView(dataSet: dataSet) - .scaleEffect(y: startAnimation ? CGFloat(DataFunctions.dataSetMaxValue(from: dataSet) / chartData.getMaxValue()) : 0, anchor: .bottom) - .scaleEffect(x: dataSet.style.barWidth, anchor: .center) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = false - } - } - } - - } else { CustomNoDataView(chartData: chartData) } - } -} - -/** - - */ -internal struct MultiPartBarSubView: View { - - private let dataSet : BarDataSet - - internal init(dataSet: BarDataSet) { - self.dataSet = dataSet - } - - internal var body: some View { - GeometryReader { geo in - - VStack(spacing: 0) { - ForEach(dataSet.dataPoints.reversed()) { dataPoint in - - if dataPoint.colourType == .colour, - let colour = dataPoint.colour - { - - ColourPartBar(colour, getHeight(height : geo.size.height, - dataSet : dataSet, - dataPoint : dataPoint)) - - } else if dataPoint.colourType == .gradientColour, - let colours = dataPoint.colours, - let startPoint = dataPoint.startPoint, - let endPoint = dataPoint.endPoint - { - - GradientColoursPartBar(colours, startPoint, endPoint, getHeight(height: geo.size.height, - dataSet : dataSet, - dataPoint : dataPoint)) - - } else if dataPoint.colourType == .gradientStops, - let stops = dataPoint.stops, - let startPoint = dataPoint.startPoint, - let endPoint = dataPoint.endPoint - { - - let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - - GradientStopsPartBar(safeStops, startPoint, endPoint, getHeight(height: geo.size.height, - dataSet : dataSet, - dataPoint : dataPoint)) - } - - } - } - } - } - - - private func getHeight(height: CGFloat, dataSet: BarDataSet, dataPoint: BarChartDataPoint) -> CGFloat { - let value = dataPoint.value - let sum = dataSet.dataPoints.reduce(0) { $0 + $1.value } - return height * CGFloat(value / sum) - } -} +//public struct StackedBarChart: View where ChartData: StackedBarChartData { +// +// @ObservedObject var chartData: ChartData +// +// 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 +// +// MultiPartBarSubView(dataSet: dataSet) +// .scaleEffect(y: startAnimation ? CGFloat(DataFunctions.dataSetMaxValue(from: dataSet) / chartData.getMaxValue()) : 0, anchor: .bottom) +// .scaleEffect(x: chartData.barStyle.barWidth, anchor: .center) +// .animateOnAppear(using: chartData.chartStyle.globalAnimation) { +// self.startAnimation = true +// } +// .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { +// self.startAnimation = false +// } +// } +// } +// +// } else { CustomNoDataView(chartData: chartData) } +// } +//} +// +///** +// +// */ +//internal struct MultiPartBarSubView: View { +// +// private let dataSet : MultiBarDataSet +// +// internal init(dataSet: MultiBarDataSet) { +// self.dataSet = dataSet +// } +// +// internal var body: some View { +// GeometryReader { geo in +// +// VStack(spacing: 0) { +// ForEach(dataSet.dataPoints.reversed()) { dataPoint in +// +// if dataPoint.colourType == .colour, +// let colour = dataPoint.colour +// { +// +// ColourPartBar(colour, getHeight(height : geo.size.height, +// dataSet : dataSet, +// dataPoint : dataPoint)) +// +// } else if dataPoint.colourType == .gradientColour, +// let colours = dataPoint.colours, +// let startPoint = dataPoint.startPoint, +// let endPoint = dataPoint.endPoint +// { +// +// GradientColoursPartBar(colours, startPoint, endPoint, getHeight(height: geo.size.height, +// dataSet : dataSet, +// dataPoint : dataPoint)) +// +// } else if dataPoint.colourType == .gradientStops, +// let stops = dataPoint.stops, +// let startPoint = dataPoint.startPoint, +// let endPoint = dataPoint.endPoint +// { +// +// let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) +// +// GradientStopsPartBar(safeStops, startPoint, endPoint, getHeight(height: geo.size.height, +// dataSet : dataSet, +// dataPoint : dataPoint)) +// } +// +// } +// } +// } +// } +// +// +// private func getHeight(height: CGFloat, dataSet: MultiBarDataSet, dataPoint: MultiPartBarChartDataPoint) -> CGFloat { +// let value = dataPoint.value +// let sum = dataSet.dataPoints.reduce(0) { $0 + $1.value } +// return height * CGFloat(value / sum) +// } +//} diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift index 26b44429..474dc4a7 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift @@ -10,36 +10,37 @@ import SwiftUI /** Bar segment where the colour information comes from chart style. */ -internal struct BarChartDataSetSubView: View { +internal struct BarChartDataSetSubView: View { - let colourType : ColourType + let chartData : CD let dataPoint : BarChartDataPoint - let style : BarStyle - let chartStyle : BarChartStyle - let maxValue : Double + + @State private var startAnimation : Bool = false internal var body: some View { - if colourType == .colour, - let colour = style.colour + if chartData.barStyle.colourType == .colour, + let colour = chartData.barStyle.colour { - ColourBar(colour, dataPoint, maxValue, chartStyle, style) - - } else if colourType == .gradientColour, - let colours = style.colours, - let startPoint = style.startPoint, - let endPoint = style.endPoint + + ColourBar(colour, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + + } else if chartData.barStyle.colourType == .gradientColour, + let colours = chartData.barStyle.colours, + let startPoint = chartData.barStyle.startPoint, + let endPoint = chartData.barStyle.endPoint { + + GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) - GradientColoursBar(colours, startPoint, endPoint, dataPoint, maxValue, chartStyle, style) - - } else if colourType == .gradientStops, - let stops = style.stops, - let startPoint = style.startPoint, - let endPoint = style.endPoint + } else if chartData.barStyle.colourType == .gradientStops, + let stops = chartData.barStyle.stops, + let startPoint = chartData.barStyle.startPoint, + let endPoint = chartData.barStyle.endPoint { let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, maxValue, chartStyle, style) + + GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) } } @@ -48,13 +49,10 @@ internal struct BarChartDataSetSubView: View { /** Bar segment where the colour information comes from datapoints. */ -internal struct BarChartDataPointSubView: View { +internal struct BarChartDataPointSubView: View { - let colourType : ColourType + let chartData : CD let dataPoint : BarChartDataPoint - let style : BarStyle - let chartStyle : BarChartStyle - let maxValue : Double internal var body: some View { @@ -62,7 +60,7 @@ internal struct BarChartDataPointSubView: View { let colour = dataPoint.colour { - ColourBar(colour, dataPoint, maxValue, chartStyle, style) + ColourBar(colour, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) } else if dataPoint.colourType == .gradientColour, let colours = dataPoint.colours, @@ -70,7 +68,7 @@ internal struct BarChartDataPointSubView: View { let endPoint = dataPoint.endPoint { - GradientColoursBar(colours, startPoint, endPoint, dataPoint, maxValue, chartStyle, style) + GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) } else if dataPoint.colourType == .gradientStops, let stops = dataPoint.stops, @@ -80,7 +78,7 @@ internal struct BarChartDataPointSubView: View { let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, maxValue, chartStyle, style) + GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) } } diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift index 1167def8..1e87598a 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift @@ -8,37 +8,41 @@ import SwiftUI // MARK: Standard -internal struct ColourBar: View { +internal struct ColourBar: View { private let colour : Color - private let data : BarChartDataPoint + private let dataPoint : DP private let maxValue : Double private let chartStyle : BarChartStyle - private let style : BarStyle + + private let cornerRadius: CornerRadius + private let barWidth : CGFloat internal init(_ colour : Color, - _ data : BarChartDataPoint, + _ dataPoint : DP, _ maxValue : Double, _ chartStyle : BarChartStyle, - _ style : BarStyle + _ cornerRadius: CornerRadius, + _ barWidth : CGFloat ) { self.colour = colour - self.data = data + self.dataPoint = dataPoint self.maxValue = maxValue self.chartStyle = chartStyle - self.style = style + self.cornerRadius = cornerRadius + self.barWidth = barWidth } @State private var startAnimation : Bool = false internal var body: some View { - RoundedRectangleBarShape(tl: style.cornerRadius.top, - tr: style.cornerRadius.top, - bl: style.cornerRadius.bottom, - br: style.cornerRadius.bottom) + RoundedRectangleBarShape(tl: cornerRadius.top, + tr: cornerRadius.top, + bl: cornerRadius.bottom, + br: cornerRadius.bottom) .fill(colour) - .scaleEffect(y: startAnimation ? CGFloat(data.value / maxValue) : 0, anchor: .bottom) - .scaleEffect(x: style.barWidth, anchor: .center) + .scaleEffect(y: startAnimation ? CGFloat(dataPoint.value / maxValue) : 0, anchor: .bottom) + .scaleEffect(x: barWidth, anchor: .center) .animateOnAppear(using: chartStyle.globalAnimation) { self.startAnimation = true } @@ -48,23 +52,26 @@ internal struct ColourBar: View { } } -internal struct GradientColoursBar: View { +internal struct GradientColoursBar: View { private let colours : [Color] private let startPoint : UnitPoint private let endPoint : UnitPoint - private let data : BarChartDataPoint + private let data : DP private let maxValue : Double private let chartStyle : BarChartStyle - private let style : BarStyle + + private let cornerRadius: CornerRadius + private let barWidth : CGFloat internal init(_ colours : [Color], _ startPoint : UnitPoint, _ endPoint : UnitPoint, - _ data : BarChartDataPoint, + _ data : DP, _ maxValue : Double, _ chartStyle : BarChartStyle, - _ style : BarStyle + _ cornerRadius: CornerRadius, + _ barWidth : CGFloat ) { self.colours = colours self.startPoint = startPoint @@ -72,21 +79,22 @@ internal struct GradientColoursBar: View { self.data = data self.maxValue = maxValue self.chartStyle = chartStyle - self.style = style + self.cornerRadius = cornerRadius + self.barWidth = barWidth } @State private var startAnimation : Bool = false internal var body: some View { - RoundedRectangleBarShape(tl: style.cornerRadius.top, - tr: style.cornerRadius.top, - bl: style.cornerRadius.bottom, - br: style.cornerRadius.bottom) + RoundedRectangleBarShape(tl: cornerRadius.top, + tr: cornerRadius.top, + bl: cornerRadius.bottom, + br: 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) + .scaleEffect(x: barWidth, anchor: .center) .animateOnAppear(using: chartStyle.globalAnimation) { self.startAnimation = true } @@ -96,23 +104,26 @@ internal struct GradientColoursBar: View { } } -internal struct GradientStopsBar: View { +internal struct GradientStopsBar: View { private let stops : [Gradient.Stop] private let startPoint : UnitPoint private let endPoint : UnitPoint - private let data : BarChartDataPoint + private let data : DP private let maxValue : Double private let chartStyle : BarChartStyle - private let style : BarStyle + + private let cornerRadius: CornerRadius + private let barWidth : CGFloat internal init(_ stops : [Gradient.Stop], _ startPoint : UnitPoint, _ endPoint : UnitPoint, - _ data : BarChartDataPoint, + _ data : DP, _ maxValue : Double, _ chartStyle : BarChartStyle, - _ style : BarStyle + _ cornerRadius: CornerRadius, + _ barWidth : CGFloat ) { self.stops = stops self.startPoint = startPoint @@ -120,21 +131,22 @@ internal struct GradientStopsBar: View { self.data = data self.maxValue = maxValue self.chartStyle = chartStyle - self.style = style + self.cornerRadius = cornerRadius + self.barWidth = barWidth } @State private var startAnimation : Bool = false internal var body: some View { - RoundedRectangleBarShape(tl: style.cornerRadius.top, - tr: style.cornerRadius.top, - bl: style.cornerRadius.bottom, - br: style.cornerRadius.bottom) + RoundedRectangleBarShape(tl: cornerRadius.top, + tr: cornerRadius.top, + bl: cornerRadius.bottom, + br: 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) + .scaleEffect(x: barWidth, anchor: .center) .animateOnAppear(using: chartStyle.globalAnimation) { self.startAnimation = true } @@ -144,8 +156,10 @@ internal struct GradientStopsBar: View { } } -// MARK: - Multi Part + + +// MARK: - Multi Part internal struct ColourPartBar: View { private let colour : Color diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index 1217d553..c4edc99a 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -43,7 +43,6 @@ public protocol LineChartDataProtocol: LineAndBarChartData where CTStyle: CTLine - Returns: The position to place the indicator. */ func getIndicatorLocation(rect: CGRect, dataSet: LineDataSet, touchLocation: CGPoint) -> CGPoint - } // MARK: - Style @@ -61,6 +60,7 @@ public protocol CTLineChartStyle : CTLineAndBarChartStyle { var baseline: Baseline { get set } } +// MARK: - DataSet /** A protocol to extend functionality of `SingleDataSet` specifically for Line Charts. @@ -70,6 +70,11 @@ public protocol CTLineChartStyle : CTLineAndBarChartStyle { - Tag: CTLineChartDataSet */ public protocol CTLineChartDataSet: SingleDataSet { + associatedtype Styling : CTColourStyle + /** + 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. diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 72702e42..ec60c31f 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -152,7 +152,6 @@ public protocol DataSet: Hashable, Identifiable { - Tag: SingleDataSet */ public protocol SingleDataSet: DataSet { - associatedtype Styling : CTColourStyle associatedtype DataPoint : CTChartDataPoint /** @@ -167,12 +166,6 @@ public protocol SingleDataSet: DataSet { */ var legendTitle : String { get set } - - - /** - Sets the style for the Data Set (as opposed to Chart Data Style). - */ - var style : Styling { get set } } /** @@ -186,7 +179,7 @@ public protocol MultiDataSet: DataSet { Array of DataSets. [See SingleDataSet](x-source-tag://SingleDataSet) */ - var dataSets : [DataSet] { get set } + var dataSets : [DataSet] { get set } } // MARK: - Data Points diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index 64433111..61b187a6 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -36,7 +36,7 @@ internal struct LegendView: View where T: ChartData { case .bar: bar(legend) - .if(self.scaleLegend(legend: legend)) { $0.scaleEffect(1.2, anchor: .leading) } +// .if(self.scaleLegend(legend: legend)) { $0.scaleEffect(1.2, anchor: .leading) } case .pie: pie(legend) @@ -46,20 +46,14 @@ internal struct LegendView: View where T: ChartData { }.id(UUID()) } private func scaleLegend(legend: LegendData) -> Bool { - - var matched : Bool = false - chartData.infoView.touchOverlayInfo.forEach { (dataPoint) in if matched { return } - if legend.id == dataPoint.id as! UUID { matched = true } } - return matched - } From e5bc0e9bae38b2a02419c5884a8a9d5e50a9a647 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 17 Feb 2021 13:43:00 +0000 Subject: [PATCH 049/152] Add Marker Attachment and Marker Type. --- .../BarChart/Models/Style/BarChartStyle.swift | 14 +++++++++++--- .../LineChart/Extras/LineChartEnums.swift | 16 ++++++++++++++++ .../Models/Protocols/LineChartProtocols.swift | 6 ++++++ .../LineChart/Models/Style/LineChartStyle.swift | 11 +++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift index f19b062e..b7310dc1 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift @@ -67,7 +67,8 @@ public struct BarChartStyle: CTBarChartStyle { public var infoBoxPlacement : InfoBoxPlacement public var infoBoxValueColour : Color public var infoBoxDescriptionColor : Color - + public var markerType : MarkerType + public var xAxisGridStyle : GridStyle public var xAxisLabelPosition : XAxisLabelPosistion public var xAxisLabelColour : Color @@ -85,18 +86,24 @@ public struct BarChartStyle: CTBarChartStyle { /// - infoBoxPlacement: Placement of the information box that appears on touch input. /// - infoBoxValueColour: Colour of the value part of the touch info. /// - infoBoxDescriptionColor: Colour of the description part 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. + /// /// - 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. + /// /// - globalAnimation: Gobal control of animations. public init(infoBoxPlacement : InfoBoxPlacement = .floating, infoBoxValueColour : Color = Color.primary, infoBoxDescriptionColor : Color = Color.primary, + markerType : MarkerType = .full, xAxisGridStyle : GridStyle = GridStyle(), xAxisLabelPosition : XAxisLabelPosistion = .bottom, @@ -111,8 +118,9 @@ public struct BarChartStyle: CTBarChartStyle { globalAnimation : Animation = Animation.linear(duration: 1) ) { self.infoBoxPlacement = infoBoxPlacement - self.infoBoxValueColour = infoBoxValueColour - self.infoBoxDescriptionColor = infoBoxDescriptionColor + self.infoBoxValueColour = infoBoxValueColour + self.infoBoxDescriptionColor = infoBoxDescriptionColor + self.markerType = markerType self.xAxisGridStyle = xAxisGridStyle self.xAxisLabelPosition = xAxisLabelPosition diff --git a/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift b/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift index 934e24d6..9a20cfe1 100644 --- a/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift +++ b/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift @@ -79,3 +79,19 @@ public enum PointShape { /// Rounded Square Shape case roundSquare } + +/** + Where the Y and X touch markers should attach themselves to. + ``` + case line // Attached to the line. + case point // Attached to the data points. + ``` + + - Tag: MarkerAttachemnt + */ +public enum MarkerAttachemnt { + /// Attached to the line. + case line + /// Attached to the data points. + case point +} diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index c4edc99a..388423f7 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -58,6 +58,12 @@ public protocol CTLineChartStyle : CTLineAndBarChartStyle { [See Baseline](x-source-tag://Baseline) */ var baseline: Baseline { get set } + + /** + Where the Y and X touch markers should attach themselves to. + */ + var markerAttachemnt : MarkerAttachemnt { get set } + } // MARK: - DataSet diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift index fec45d60..cd689a98 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift @@ -72,6 +72,8 @@ public struct LineChartStyle: CTLineChartStyle { public var infoBoxPlacement : InfoBoxPlacement public var infoBoxValueColour : Color public var infoBoxDescriptionColor : Color + public var markerType : MarkerType + public var markerAttachemnt : MarkerAttachemnt public var xAxisGridStyle : GridStyle public var xAxisLabelPosition : XAxisLabelPosistion @@ -92,6 +94,9 @@ public struct LineChartStyle: CTLineChartStyle { /// - infoBoxValueColour: Colour of the value part of the touch info. /// - infoBoxDescriptionColor: Colour of the description part of the touch info. /// + /// - markerType: Where the marker lines come from to meet at a specified point. + /// - markerAttachemnt: Where the Y and X touch markers should attach themselves to. + /// /// - 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. @@ -108,6 +113,9 @@ public struct LineChartStyle: CTLineChartStyle { infoBoxValueColour : Color = Color.primary, infoBoxDescriptionColor : Color = Color.primary, + markerType : MarkerType = .vertical, + markerAttachemnt : MarkerAttachemnt = .line, + xAxisGridStyle : GridStyle = GridStyle(), xAxisLabelPosition : XAxisLabelPosistion = .bottom, xAxisLabelColour : Color = Color.primary, @@ -125,6 +133,9 @@ public struct LineChartStyle: CTLineChartStyle { self.infoBoxValueColour = infoBoxValueColour self.infoBoxDescriptionColor = infoBoxDescriptionColor + self.markerType = markerType + self.markerAttachemnt = markerAttachemnt + self.xAxisGridStyle = xAxisGridStyle self.xAxisLabelPosition = xAxisLabelPosition self.xAxisLabelsFrom = xAxisLabelsFrom From 1fe01c39283f0ba6e2e2c8d483a62834002b27df Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 17 Feb 2021 13:43:28 +0000 Subject: [PATCH 050/152] Add Marker Type. --- .../Models/Protocols/LineAndBarProtocols.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index b06df499..ca527ef4 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -94,6 +94,11 @@ public protocol LineAndBarChartData : ChartData where CTStyle: CTLineAndBarChart - Tag: CTLineAndBarChartStyle */ public protocol CTLineAndBarChartStyle: CTChartStyle { + /** + Where the marker lines come from to meet at a specified point. + */ + var markerType : MarkerType { get set } + /** Style of the vertical lines breaking up the chart From afa4d9fc75ce80dc5de18b81bb67dd1aa7c6a682 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 17 Feb 2021 13:44:03 +0000 Subject: [PATCH 051/152] Add getPointLocation functionality. --- .../ChartData/GroupedBarChartData.swift | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 113cca3c..9eec5985 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -297,7 +297,36 @@ public class GroupedBarChartData: BarChartDataProtocol { } public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - let locations : [HashablePoint] = [] + var locations : [HashablePoint] = [] + + // Divide the chart into equal sections. + let superXSection : CGFloat = (chartSize.size.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.size.width / CGFloat(dataSets.dataSets.count)) - compensation + let ySection : CGFloat = chartSize.size.height / CGFloat(self.getMaxValue()) + + 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(index))) / xSubSection) - (dataSet.dataPoints.count * index) + + if subIndex >= 0 && subIndex < dataSet.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)) + locations.append(HashablePoint(x: element + section + spacing, + y: (chartSize.size.height - CGFloat(dataSet.dataPoints[subIndex].value) * ySection))) + + } + } return locations } From a69b27a07d5836c6dcb915980775c1a10f737902 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 17 Feb 2021 13:47:07 +0000 Subject: [PATCH 052/152] Neaten Touch overlay markers. Add vertical and rectangle option. --- .../Shared/Extras/SharedEnums.swift | 31 ++- .../Shared/Shapes/TouchOverlayMarker.swift | 188 ++++++++++++++---- 2 files changed, 171 insertions(+), 48 deletions(-) diff --git a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift index d715274c..21b7cf07 100644 --- a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift @@ -66,6 +66,7 @@ public enum ColourType { Placement of the data point information panel when touch overlay modifier is applied. ``` case floating // Follows input across the chart + case fixed case header // Fix in the Header box. Must have .headerBox() ``` @@ -74,6 +75,7 @@ public enum ColourType { public enum InfoBoxPlacement { /// Follows input across the chart case floating + case fixed /// Fix in the Header box. Must have .headerBox() case header } @@ -82,24 +84,31 @@ public enum InfoBoxPlacement { /** 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 + case vertical // Vertical line from top to bottom. + case rectangle // Rounded rectangle. + case full // 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. + ``` - Tag: MarkerType */ public enum MarkerType { - /// Full width and height of view intersecting at a specified point - case fullWidth - /// From bottom and leading edges meeting at a specified point + /// Vertical line from top to bottom. + case vertical + /// Rounded rectangle. + case rectangle + /// 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 + /// From bottom and trailing edges meeting at a specified point. case bottomTrailing - /// From top and leading edges meeting at a specified point + /// From top and leading edges meeting at a specified point. case topLeading - /// From top and trailing edges meeting at a specified point + /// From top and trailing edges meeting at a specified point. case topTrailing } diff --git a/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift b/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift index 64b7aaee..38eb6792 100644 --- a/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift +++ b/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift @@ -7,57 +7,171 @@ 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 : MarkerType = .fullWidth - /// Point that the marker lines should intersect private var position : CGPoint - - internal init(type : MarkerType = .fullWidth, - position : HashablePoint - ) { - self.type = type + + @inlinable internal init(position : HashablePoint) { self.position = CGPoint(x: position.x, y: position.y) } + @inlinable 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 + + @inlinable internal init(position : HashablePoint) { + self.position = CGPoint(x: position.x, y: position.y) + } + + @inlinable 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 + + @inlinable internal init(position : HashablePoint) { + self.position = CGPoint(x: position.x, y: position.y) + } + + @inlinable 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 + + @inlinable internal init(position : HashablePoint) { + self.position = CGPoint(x: position.x, y: position.y) + } + + @inlinable 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 + + @inlinable internal init(position : HashablePoint) { + self.position = CGPoint(x: position.x, y: position.y) + } + + @inlinable 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 + + @inlinable internal init(position : HashablePoint) { + self.position = CGPoint(x: position.x, y: position.y) + } + + @inlinable 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 363cf056cbf7bf74b0c53bec41c8293eb22afee5 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 17 Feb 2021 13:48:29 +0000 Subject: [PATCH 053/152] Add the ability to pass width data to InfoBox. --- .../ViewModifiers/YAxisLabels.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift index 19f0f1ad..24dfff99 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift @@ -36,7 +36,6 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { } internal var labels: some View { - VStack { if labelsAndTop { textAsSpacer @@ -58,6 +57,16 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { } .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 + } + } + ) } internal func body(content: Content) -> some View { @@ -65,12 +74,12 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { if chartData.isGreaterThanTwo() { switch chartData.chartStyle.yAxisLabelPosition { case .leading: - HStack { + HStack(spacing: 0) { labels content } case .trailing: - HStack { + HStack(spacing: 0) { content labels } From d053ae8946e04f45ba4f4020dff052390669e6b8 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 17 Feb 2021 13:49:26 +0000 Subject: [PATCH 054/152] Add infoBox view modifier. --- .../Shared/Models/InfoViewData.swift | 3 + .../Shared/ViewModifiers/HeaderBox.swift | 11 ++- .../Shared/ViewModifiers/InfoBox.swift | 86 +++++++++++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift diff --git a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift index aa1ace94..31b29e14 100644 --- a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift @@ -42,4 +42,7 @@ public struct InfoViewData { */ var touchSpecifier : String = "%.0f" + var positionX : CGFloat = 0 + var frame : CGRect = .zero + var yAxisLabelWidth: CGFloat = 0 } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index 648ede20..d6b906bf 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -52,12 +52,19 @@ internal struct HeaderBox: ViewModifier where T: ChartData { Group { #if !os(tvOS) if chartData.isGreaterThanTwo() { - if chartData.getHeaderLocation() == .floating { + + switch chartData.getHeaderLocation() { + case .floating: VStack(alignment: .leading) { titleBox content } - } else if chartData.getHeaderLocation() == .header { + case .fixed: + VStack(alignment: .leading) { + titleBox + content + } + case .header: VStack(alignment: .leading) { HStack(spacing: 0) { HStack(spacing: 0) { diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift new file mode 100644 index 00000000..9d51f608 --- /dev/null +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift @@ -0,0 +1,86 @@ +// +// InfoBox.swift +// +// +// Created by Will Dale on 15/02/2021. +// + +import SwiftUI + +internal struct InfoBox: ViewModifier where T: ChartData { + + @ObservedObject var chartData: T + + @State private var boxFrame : CGRect = CGRect(x: 0, y: 0, width: 0, height: 50) + + internal func body(content: Content) -> some View { + VStack { + switch chartData.getHeaderLocation() { + case .floating: + floating + case .fixed: + fixed + case .header: + EmptyView() + } + content + } + } + + var floating: some View { + TouchOverlayBox(isTouchCurrent : chartData.infoView.isTouchCurrent, + selectedPoints : chartData.infoView.touchOverlayInfo, + specifier : chartData.infoView.touchSpecifier, + valueColour : chartData.chartStyle.infoBoxValueColour, + descriptionColour: chartData.chartStyle.infoBoxDescriptionColor, + boxFrame : $boxFrame) + .position(x: setBoxLocationation(touchLocation: chartData.infoView.positionX, + boxFrame : boxFrame, + chartSize : chartData.infoView.frame), + y: 15) + .frame(height: 40) + } + + + var fixed: some View { + LazyHGrid(rows: [GridItem(.flexible())]) { + ForEach(chartData.infoView.touchOverlayInfo, id: \.self) { point in + HStack { + Text("\(point.value, specifier: chartData.infoView.touchSpecifier)") + .font(.body) + .foregroundColor(chartData.chartStyle.infoBoxValueColour) + if let label = point.pointDescription { + Text(label) + .font(.body) + .foregroundColor(chartData.chartStyle.infoBoxDescriptionColor) + } + } + } + }.frame(height: 40) + } + + + /// Sets the 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 + chartData.infoView.yAxisLabelWidth + } +} + +extension View { + public func infoBox(chartData: T) -> some View { + self.modifier(InfoBox(chartData: chartData)) + } +} From 30561b241bf2d087f826bd180d1ba5e9d8690ba4 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 17 Feb 2021 13:50:01 +0000 Subject: [PATCH 055/152] Tidy up. --- .../Models/Protocols/SharedProtocols.swift | 1 + .../Shared/Views/TouchOverlayBox.swift | 49 +++++++++++-------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index ec60c31f..dc27c7a3 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -113,6 +113,7 @@ public protocol ChartData: ObservableObject, Identifiable { */ func isGreaterThanTwo() -> Bool + // MARK: Touch /** Gets the nearest data points to the touch location. - Parameters: diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index b000b406..ea02f17d 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -9,43 +9,50 @@ import SwiftUI internal struct TouchOverlayBox: View { - private var selectedPoints : [D] - private var specifier : String + private var isTouchCurrent : Bool + private var selectedPoints : [D] + private var specifier : String - private let valueColour : Color - private let descriptionColour : Color + private var valueColour : Color + private var descriptionColour : Color - private var ignoreZero : Bool + private var ignoreZero : Bool @Binding private var boxFrame : CGRect - internal init(selectedPoints : [D], - specifier : String = "%.0f", - valueColour : Color, - descriptionColour : Color, - boxFrame : Binding, - ignoreZero : Bool = false + internal init(isTouchCurrent : Bool, + selectedPoints : [D], + specifier : String = "%.0f", + valueColour : Color, + descriptionColour : Color, + boxFrame : Binding, + ignoreZero : Bool = false ) { - self.selectedPoints = selectedPoints - self.specifier = specifier - self.valueColour = valueColour - self.descriptionColour = descriptionColour - self._boxFrame = boxFrame - self.ignoreZero = ignoreZero + self.isTouchCurrent = isTouchCurrent + self.selectedPoints = selectedPoints + self.specifier = specifier + self.valueColour = valueColour + self.descriptionColour = descriptionColour + self._boxFrame = boxFrame + self.ignoreZero = ignoreZero } internal var body: some View { - VStack { + + HStack { ForEach(selectedPoints, id: \.self) { point in if ignoreZero && point.value != 0 { Text("\(point.value, specifier: specifier)") + .font(.body) .foregroundColor(valueColour) } else if !ignoreZero { Text("\(point.value, specifier: specifier)") + .font(.body) .foregroundColor(valueColour) } if let label = point.pointDescription { Text(label) + .font(.body) .foregroundColor(descriptionColour) } } @@ -53,10 +60,11 @@ internal struct TouchOverlayBox: View { .padding(.all, 8) .background( GeometryReader { geo in + if isTouchCurrent { 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) +// .shadow(color: Color(.systemGray), radius: 6, x: 0, y: 0) RoundedRectangle(cornerRadius: 15.0, style: .continuous) .fill(Color(.systemBackground)) #elseif os(macOS) @@ -82,6 +90,7 @@ internal struct TouchOverlayBox: View { self.boxFrame = frame } } + } ) } } From a23319e7ac06ec5e40001f064f9f0dee8a0a652b Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 17 Feb 2021 13:52:13 +0000 Subject: [PATCH 056/152] Overall how touch interactions a handled. Pass functionality away to the protocols. --- .../Shared/ViewModifiers/TouchOverlay.swift | 284 ++++++++++++------ 1 file changed, 192 insertions(+), 92 deletions(-) diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index e9b3e2cb..d637c65a 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -17,110 +17,229 @@ import SwiftUI internal struct TouchOverlay: ViewModifier where T: ChartData { @ObservedObject var chartData: T - - /// Decimal precision for labels - private let specifier : String + + private var markerType : MarkerType /// Current location of the touch input @State private var touchLocation : CGPoint = CGPoint(x: 0, y: 0) - /// The data point closest to the touch input - @State private var selectedPoints : [T.DataPoint] = [] - /// The location for the nearest data point to the touch input - @State private var pointLocations : [HashablePoint] = [HashablePoint(x: 0, y: 0)] /// Frame information of the data point information box @State private var boxFrame : CGRect = CGRect(x: 0, y: 0, width: 0, height: 50) - /// Placement of the data point information box - @State private var boxLocation : CGPoint = CGPoint(x: 0, y: 0) - /// Placement of place the markers intersecting the data points location - @State private var markerLocation : CGPoint = CGPoint(x: 0, y: 0) /// Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information. /// - Parameters: /// - chartData: /// - specifier: Decimal precision for labels internal init(chartData : T, - specifier : String + specifier : String, + markerType : MarkerType ) { - self.chartData = chartData - self.specifier = specifier + self.chartData = chartData + self.markerType = markerType + self.chartData.infoView.touchSpecifier = specifier } internal func body(content: Content) -> some View { Group { if chartData.isGreaterThanTwo() { + GeometryReader { geo in ZStack { content .gesture( DragGesture(minimumDistance: 0) .onChanged { (value) in - touchLocation = value.location - + touchLocation = value.location + chartData.infoView.isTouchCurrent = true + chartData.infoView.touchOverlayInfo = chartData.getDataPoint(touchLocation: touchLocation, chartSize: geo) - self.selectedPoints = chartData.getDataPoint(touchLocation: touchLocation, - chartSize: geo) - self.pointLocations = chartData.getPointLocation(touchLocation: touchLocation, - chartSize: geo) - - chartData.infoView.touchOverlayInfo = selectedPoints + chartData.infoView.positionX = setBoxLocationation(touchLocation: touchLocation, boxFrame: boxFrame, chartSize: geo).x - if chartData.getHeaderLocation() == .floating { - - setBoxLocationation(boxFrame: boxFrame, chartSize: geo) - markerLocation.x = setMarkerXLocation(chartSize: geo) - markerLocation.y = setMarkerYLocation(chartSize: geo) - - } + chartData.infoView.frame = geo.frame(in: .local) } .onEnded { _ in - chartData.infoView.isTouchCurrent = false + chartData.infoView.isTouchCurrent = false chartData.infoView.touchOverlayInfo = [] } ) + /* + TODO: ------------------------------- + Choose attachement style for markers + Add touch event function to protocol + */ if chartData.infoView.isTouchCurrent { - ForEach(pointLocations, id: \.self) { location in - TouchOverlayMarker(position: location) - .stroke(Color(.gray), lineWidth: 1) - } - if chartData.getHeaderLocation() == .floating { - TouchOverlayBox(selectedPoints : selectedPoints, - specifier : specifier, - valueColour : chartData.chartStyle.infoBoxValueColour, - descriptionColour: chartData.chartStyle.infoBoxDescriptionColor, - boxFrame : $boxFrame) - .position(x: boxLocation.x, y: 0 + (boxFrame.height / 2)) - } - - // MARK: - Position Indicator - // TODO: Refactor - if chartData.chartType == (.line, .single) { - - let data = chartData as! LineChartData - + switch chartData.chartType { + case (.line, .single): + Text("") + if let data = chartData as? LineChartData { let position = data.getIndicatorLocation(rect: geo.frame(in: .global), dataSet: data.dataSets, touchLocation: touchLocation) - + + switch markerType { + case .vertical: + Vertical(position: position) + .stroke(Color.primary, lineWidth: 2) + case .rectangle: + RoundedRectangle(cornerRadius: 25.0, style: .continuous) + .fill(Color.clear) + .frame(width: 100, height: geo.frame(in: .local).height) + .position(x: position.x, + y: geo.frame(in: .local).midY) + .overlay( + RoundedRectangle(cornerRadius: 25.0, style: .continuous) + .stroke(Color.primary, lineWidth: 2) + .shadow(color: .primary, radius: 4, x: 0, y: 0) + .frame(width: 50, height: geo.frame(in: .local).height) + .position(x: position.x, + y: geo.frame(in: .local).midY) + ) + 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) + } + PosistionIndicator() .frame(width: 15, height: 15) .position(position) + + } - } else if chartData.chartType == (.line, .multi) { + case (.line, .multi): + + if let data = chartData as? MultiLineChartData { + + ForEach(data.dataSets.dataSets, id: \.self) { dataSet in + let position = data.getIndicatorLocation(rect: geo.frame(in: .global), + dataSet: dataSet, + touchLocation: touchLocation) + + switch markerType { + case .vertical: + Vertical(position: position) + .stroke(Color.primary, lineWidth: 2) + case .rectangle: + RoundedRectangle(cornerRadius: 25.0, style: .continuous) + .fill(Color.clear) + .frame(width: 100, height: geo.frame(in: .local).height) + .position(x: position.x, + y: geo.frame(in: .local).midY) + .overlay( + RoundedRectangle(cornerRadius: 25.0, style: .continuous) + .stroke(Color.primary, lineWidth: 2) + .shadow(color: .primary, radius: 4, x: 0, y: 0) + .frame(width: 50, height: geo.frame(in: .local).height) + .position(x: position.x, + y: geo.frame(in: .local).midY) + ) + 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) + } + - let data = chartData as! MultiLineChartData + PosistionIndicator() + .frame(width: 15, height: 15) + .position(position) + } + } + + case (.bar, .single): - ForEach(data.dataSets.dataSets, id: \.self) { dataSet in + if let data = chartData as? BarChartData { - let position = data.getIndicatorLocation(rect: geo.frame(in: .global), - dataSet: dataSet, - touchLocation: touchLocation) + let positions = data.getPointLocation(touchLocation: touchLocation, + chartSize: geo) + ForEach(positions, id: \.self) { position in + + switch markerType { + case .vertical: + MarkerFull(position: position) + .stroke(Color.primary, lineWidth: 2) + case .rectangle: + 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) + } + } - PosistionIndicator() - .frame(width: 15, height: 15) - .position(position) } + case (.bar, .multi): + if let data = chartData as? GroupedBarChartData { + + let positions = data.getPointLocation(touchLocation: touchLocation, + chartSize: geo) + ForEach(positions, id: \.self) { position in + + switch markerType { + case .vertical: + MarkerFull(position: position) + .stroke(Color.primary, lineWidth: 2) + case .rectangle: + 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) + } + } + } + + case (.pie, .single): + Text("") + case (.pie, .multi): + Text("") } } } @@ -133,41 +252,19 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { /// - Parameters: /// - boxFrame: The size of the point info box. /// - chartSize: The size of the chart view as the parent view. - internal func setBoxLocationation(boxFrame: CGRect, chartSize: GeometryProxy) { + internal func setBoxLocationation(touchLocation: CGPoint, boxFrame: CGRect, chartSize: GeometryProxy) -> CGPoint { + + var returnPoint : CGPoint = .zero + if touchLocation.x < chartSize.frame(in: .local).minX + (boxFrame.width / 2) { - boxLocation.x = chartSize.frame(in: .local).minX + (boxFrame.width / 2) + returnPoint.x = chartSize.frame(in: .local).minX + (boxFrame.width / 2) } else if touchLocation.x > chartSize.frame(in: .local).maxX - (boxFrame.width / 2) { - boxLocation.x = chartSize.frame(in: .local).maxX - (boxFrame.width / 2) - } else { - boxLocation.x = touchLocation.x - } - } - /// Sets the X axis marker location while keeping it within the parent view. - /// - Parameter chartSize: The size of the chart view as the parent view. - /// - Returns: Position of the marker. - internal func setMarkerXLocation(chartSize: GeometryProxy) -> CGFloat { - if touchLocation.x < chartSize.frame(in: .local).minX { - return chartSize.frame(in: .local).minX - } else if touchLocation.x > chartSize.frame(in: .local).maxX { - return chartSize.frame(in: .local).maxX + returnPoint.x = chartSize.frame(in: .local).maxX - (boxFrame.width / 2) } else { - return touchLocation.x + returnPoint.x = touchLocation.x } + return returnPoint } - /// Sets the Y axis marker location while keeping it within the parent view. - /// - Parameter chartSize: The size of the chart view as the parent view. - /// - Returns: Position of the marker. - internal func setMarkerYLocation(chartSize: GeometryProxy) -> CGFloat { - if touchLocation.y < chartSize.frame(in: .local).minY { - return chartSize.frame(in: .local).minY - } else if touchLocation.y > chartSize.frame(in: .local).maxY { - return chartSize.frame(in: .local).maxY - } else { - return touchLocation.y - } - } - - } #endif @@ -193,10 +290,12 @@ extension View { - Tag: TouchOverlay */ public func touchOverlay(chartData: T, - specifier: String = "%.0f" + specifier: String = "%.0f", + markerType: MarkerType = .vertical ) -> some View { self.modifier(TouchOverlay(chartData: chartData, - specifier: specifier)) + specifier: specifier, + markerType: markerType)) } #elseif os(tvOS) /** @@ -206,7 +305,8 @@ extension View { Unavailable in tvOS */ public func touchOverlay(chartData: T, - specifier: String = "%.0f" + specifier: String = "%.0f", + markerType: MarkerType = .fullWidth ) -> some View { self.modifier(EmptyModifier()) } From add58e5d74d05b8e3fd78f4370a5e21487db4196 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 17 Feb 2021 16:18:24 +0000 Subject: [PATCH 057/152] Mark Classes as final. --- .../BarChart/Models/ChartData/BarChartData.swift | 2 +- .../Models/ChartData/GroupedBarChartData.swift | 2 +- .../Models/ChartData/StackedBarChartData.swift | 2 +- .../LineChart/Models/ChartData/LineChartData.swift | 2 +- .../Models/ChartData/MultiLineChartData.swift | 2 +- .../LineChart/Models/DataSet/LineDataSet.swift | 2 +- .../PieChart/Models/Doughnut/DoughnutChartData.swift | 2 +- .../Models/MultiLayer/MultiLayerPieChartData.swift | 2 +- .../PieChart/Models/Pie/PieChartData.swift | 2 +- .../SharedLineAndBar/ViewModifiers/YAxisLabels.swift | 10 +++++----- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 403f206e..9d325018 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -167,7 +167,7 @@ import SwiftUI - Tag: BarChartData */ -public class BarChartData: BarChartDataProtocol { +public final class BarChartData: BarChartDataProtocol { // MARK: - Properties public let id : UUID = UUID() diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 9eec5985..5de406de 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -175,7 +175,7 @@ import SwiftUI - Tag: GroupedBarChartData */ -public class GroupedBarChartData: BarChartDataProtocol { +public final class GroupedBarChartData: BarChartDataProtocol { // MARK: - Properties public let id : UUID = UUID() diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index c43f168f..d613f7b1 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -7,7 +7,7 @@ import SwiftUI -public class StackedBarChartData: BarChartDataProtocol { +public final class StackedBarChartData: BarChartDataProtocol { // MARK: - Properties public let id : UUID = UUID() diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index 1eb2fd88..38328bff 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -153,7 +153,7 @@ import SwiftUI - Tag: LineChartData */ -public class LineChartData: LineChartDataProtocol { +public final class LineChartData: LineChartDataProtocol { // MARK: - Properties public let id : UUID = UUID() diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index 93361aec..1e76997b 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -166,7 +166,7 @@ import SwiftUI - Tag: LineChartData */ -public class MultiLineChartData: LineChartDataProtocol { +public final class MultiLineChartData: LineChartDataProtocol { // MARK: - Properties public let id : UUID = UUID() diff --git a/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift index 8d16b762..587015f0 100644 --- a/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift @@ -101,7 +101,7 @@ public struct LineDataSet: CTLineChartDataSet { /// - pointStyle: Styling information for the data point markers. /// - style: Styling for how the line will be drawin. public init(dataPoints : [LineChartDataPoint], - legendTitle : String, + legendTitle : String = "", pointStyle : PointStyle = PointStyle(), style : LineStyle = LineStyle() ) { diff --git a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift index 3d7b9d38..cddbcdb2 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift @@ -7,7 +7,7 @@ import SwiftUI -public class DoughnutChartData: DoughnutChartDataProtocol { +public final class DoughnutChartData: DoughnutChartDataProtocol { @Published public var id : UUID = UUID() @Published public var dataSets : PieDataSet diff --git a/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift index efa8246f..458d3822 100644 --- a/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift @@ -7,7 +7,7 @@ import SwiftUI -public class MultiLayerPieChartData { +public final class MultiLayerPieChartData { @Published public var id : UUID = UUID() @Published public var dataSets : MultiPieDataSet diff --git a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift index 08d37272..2d2ca48f 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift @@ -7,7 +7,7 @@ import SwiftUI -public class PieChartData: PieChartDataProtocol { +public final class PieChartData: PieChartDataProtocol { @Published public var id : UUID = UUID() @Published public var dataSets : PieDataSet diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift index 24dfff99..c6a65b51 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift @@ -11,11 +11,11 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData: T - let specifier : String - var labelsArray : [Double] { chartData.getYLabels() } - - let labelsAndTop : Bool - let labelsAndBottom : Bool + private let specifier : String + private var labelsArray : [Double] { chartData.getYLabels() } + + private let labelsAndTop : Bool + private let labelsAndBottom : Bool internal init(chartData: T, specifier: String From fe781f40eb7d5044f35205fc9a92429a83ea04bd Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 17 Feb 2021 17:40:55 +0000 Subject: [PATCH 058/152] Update tests. --- .../SwiftUIChartsTests.swift | 438 ++++++++---------- 1 file changed, 200 insertions(+), 238 deletions(-) diff --git a/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift b/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift index 7444054a..4e1159e1 100644 --- a/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift +++ b/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift @@ -3,292 +3,254 @@ import XCTest final class SwiftUIChartsTests: XCTestCase { - // MARK: - ChartData + // MARK: - Single Line Data func testMaxValue() { let dataPoints = [ - ChartDataPoint(value: 10), - ChartDataPoint(value: 40), - ChartDataPoint(value: 30), - ChartDataPoint(value: 60) + LineChartDataPoint(value: 10), + LineChartDataPoint(value: 40), + LineChartDataPoint(value: 30), + LineChartDataPoint(value: 60) ] - let chartData = ChartData(dataPoints: dataPoints) + let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) - XCTAssertEqual(chartData.maxValue(), 60) + XCTAssertEqual(chartData.getMaxValue(), 60) } func testMinValue() { let dataPoints = [ - ChartDataPoint(value: 10), - ChartDataPoint(value: 40), - ChartDataPoint(value: 30), - ChartDataPoint(value: 60) + LineChartDataPoint(value: 10), + LineChartDataPoint(value: 40), + LineChartDataPoint(value: 30), + LineChartDataPoint(value: 60) ] - let chartData = ChartData(dataPoints: dataPoints) + let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) - XCTAssertEqual(chartData.minValue(), 10) + XCTAssertEqual(chartData.getMinValue(), 10) } func testAverage() { let dataPoints = [ - ChartDataPoint(value: 10), - ChartDataPoint(value: 40), - ChartDataPoint(value: 30), - ChartDataPoint(value: 60) + LineChartDataPoint(value: 10), + LineChartDataPoint(value: 40), + LineChartDataPoint(value: 30), + LineChartDataPoint(value: 60) ] - let chartData = ChartData(dataPoints: dataPoints) + let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) - XCTAssertEqual(chartData.average(), 35) + XCTAssertEqual(chartData.getAverage(), 35) } func testRange() { let dataPoints = [ - ChartDataPoint(value: 10), - ChartDataPoint(value: 40), - ChartDataPoint(value: 30), - ChartDataPoint(value: 60) + LineChartDataPoint(value: 10), + LineChartDataPoint(value: 40), + LineChartDataPoint(value: 30), + LineChartDataPoint(value: 60) ] - let chartData = ChartData(dataPoints: dataPoints) + let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) - XCTAssertEqual(chartData.range(), 50.001) + XCTAssertEqual(chartData.getRange(), 50.001) } - // MARK: - Calculations - func testMonthlyAverage() { - let calendar = Calendar.current + + + // MARK: - Multi Line Data + func testMultiLineMaxValue() { - let formatterForXAxisLabel = DateFormatter() - formatterForXAxisLabel.locale = .current - formatterForXAxisLabel.setLocalizedDateFormatFromTemplate("MMM") - let formatterForPointLabel = DateFormatter() - formatterForPointLabel.locale = .current - formatterForPointLabel.setLocalizedDateFormatFromTemplate("MMMM YYYY") + let chartData = MultiLineChartData(dataSets: + MultiLineDataSet(dataSets: [ + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 10), + LineChartDataPoint(value: 40), + LineChartDataPoint(value: 30), + LineChartDataPoint(value: 60) + ], + legendTitle: "Bob"), + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 50), + LineChartDataPoint(value: 60), + LineChartDataPoint(value: 80), + LineChartDataPoint(value: 100) + ], + legendTitle: "Bob") + ])) - let components = DateComponents(year: 2021, month: 01, day: 01, hour: 10, minute: 0, second: 0) + XCTAssertEqual(chartData.getMaxValue(), 100) + } + func testMultiLineMinValue() { + let chartData = MultiLineChartData(dataSets: + MultiLineDataSet(dataSets: [ + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 10), + LineChartDataPoint(value: 40), + LineChartDataPoint(value: 30), + LineChartDataPoint(value: 60) + ], + legendTitle: "Bob"), + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 50), + LineChartDataPoint(value: 60), + LineChartDataPoint(value: 80), + LineChartDataPoint(value: 100) + ], + legendTitle: "Bob") + ])) + + XCTAssertEqual(chartData.getMinValue(), 10) + } + func testMultiLineAverage() { + let chartData = MultiLineChartData(dataSets: + MultiLineDataSet(dataSets: [ + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 10), + LineChartDataPoint(value: 40), + LineChartDataPoint(value: 30), + LineChartDataPoint(value: 60) + ], + legendTitle: "Bob"), + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 50), + LineChartDataPoint(value: 60), + LineChartDataPoint(value: 80), + LineChartDataPoint(value: 100) + ], + legendTitle: "Bob") + ])) - guard let date = calendar.date(from: components) else { - XCTFail("date failed") - return - } - guard let monthOne = calendar.date(byAdding: .month, value: 0, to: date) else { - XCTFail("monthOne failed") - return - } - guard let monthTwo = calendar.date(byAdding: .month, value: 1, to: date) else { - XCTFail("monthTwo failed") - return - } - guard let monthThree = calendar.date(byAdding: .month, value: 2, to: date) else { - XCTFail("monthThree failed") - return - } - guard let monthFour = calendar.date(byAdding: .month, value: 3, to: date) else { - XCTFail("monthFour failed") - return - } + XCTAssertEqual(chartData.getAverage(), 53.75) + } + func testMultiLineRange() { + let chartData = MultiLineChartData(dataSets: + MultiLineDataSet(dataSets: [ + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 10), + LineChartDataPoint(value: 40), + LineChartDataPoint(value: 30), + LineChartDataPoint(value: 60) + ], + legendTitle: "Bob"), + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 50), + LineChartDataPoint(value: 60), + LineChartDataPoint(value: 80), + LineChartDataPoint(value: 100) + ], + legendTitle: "Bob") + ])) + XCTAssertEqual(chartData.getRange(), 90.001) + } + // MARK: - Labels + func testLineGetYLabels() { 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: 6, to: monthTwo)), - ChartDataPoint(value: 70, date: calendar.date(byAdding: .day, value: 19, to: monthTwo)), - ChartDataPoint(value: 30, date: calendar.date(byAdding: .day, value: 27, to: monthTwo)), - - ChartDataPoint(value: 20, date: calendar.date(byAdding: .day, value: 0, to: monthThree)), - ChartDataPoint(value: 50, date: calendar.date(byAdding: .day, value: 3, to: monthThree)), - ChartDataPoint(value: 40, date: calendar.date(byAdding: .day, value: 10, to: monthThree)), - ChartDataPoint(value: 80, date: calendar.date(byAdding: .day, value: 20, to: monthThree)), - - ChartDataPoint(value: 70, date: calendar.date(byAdding: .day, value: 0, to: monthFour)), - ChartDataPoint(value: 40, date: calendar.date(byAdding: .day, value: 2, to: monthFour)), - ChartDataPoint(value: 20, date: calendar.date(byAdding: .day, value: 25, to: monthFour)), - ChartDataPoint(value: 10, date: calendar.date(byAdding: .day, value: 26, to: monthFour)) + LineChartDataPoint(value: 10), + LineChartDataPoint(value: 50), + LineChartDataPoint(value: 40), + LineChartDataPoint(value: 80) ] - - guard let monthlyAverage = Calculations.monthlyAverage(dataPoints: dataPoints) else { - XCTFail("Failed") - return - } + let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints), + chartStyle: LineChartStyle(yAxisNumberOfLabels: 3)) + XCTAssertEqual(chartData.getYLabels()[0], 10.0000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[1], 33.3333, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[2], 56.6666, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[3], 80.0000, accuracy: 0.01) - XCTAssertEqual(monthlyAverage[0].value, 35.0) - XCTAssertEqual(monthlyAverage[1].value, 52.5) - XCTAssertEqual(monthlyAverage[2].value, 47.5) - XCTAssertEqual(monthlyAverage[3].value, 35.0) - - XCTAssertEqual(monthlyAverage[0].xAxisLabel, formatterForXAxisLabel.string(from: monthOne)) - XCTAssertEqual(monthlyAverage[1].xAxisLabel, formatterForXAxisLabel.string(from: monthTwo)) - XCTAssertEqual(monthlyAverage[2].xAxisLabel, formatterForXAxisLabel.string(from: monthThree)) - XCTAssertEqual(monthlyAverage[3].xAxisLabel, formatterForXAxisLabel.string(from: monthFour)) + } + func testBarGetYLabels() { + let dataPoints = [ + BarChartDataPoint(value: 10), + BarChartDataPoint(value: 50), + BarChartDataPoint(value: 40), + BarChartDataPoint(value: 80) + ] + let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints, legendTitle: "Test"), + chartStyle: BarChartStyle(yAxisNumberOfLabels: 3)) + + XCTAssertEqual(chartData.getYLabels()[0], 0.00000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[1], 26.6666, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[2], 53.3333, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[3], 80.0000, accuracy: 0.01) - XCTAssertEqual(monthlyAverage[0].pointDescription, formatterForPointLabel.string(from: monthOne)) - XCTAssertEqual(monthlyAverage[1].pointDescription, formatterForPointLabel.string(from: monthTwo)) - XCTAssertEqual(monthlyAverage[2].pointDescription, formatterForPointLabel.string(from: monthThree)) - XCTAssertEqual(monthlyAverage[3].pointDescription, formatterForPointLabel.string(from: monthFour)) } - func testWeeklyAverage() { - let calendar = Calendar.current - - let formatterForXAxisLabel = DateFormatter() - formatterForXAxisLabel.locale = .current - formatterForXAxisLabel.setLocalizedDateFormatFromTemplate("d") - let formatterForPointLabel = DateFormatter() - formatterForPointLabel.locale = .current - formatterForPointLabel.setLocalizedDateFormatFromTemplate("MMMM YYYY") - - let components = DateComponents(year: 2021, month: 01, day: 03, hour: 10, minute: 0, second: 0) - - guard let date = calendar.date(from: components) else { - XCTFail("date failed") - return - } - guard let weekOne = calendar.date(byAdding: .day, value: 1, to: date) else { - XCTFail("monthOne failed") - return - } - guard let weekTwo = calendar.date(byAdding: .day, value: 8, to: date) else { - XCTFail("monthTwo failed") - return - } - guard let weekThree = calendar.date(byAdding: .day, value: 15, to: date) else { - XCTFail("monthThree failed") - return - } - guard let weekFour = calendar.date(byAdding: .day, value: 22, to: date) else { - XCTFail("monthFour failed") - return - } - + // MARK: - Chart Data + func testIsGreaterThanTwoTrue() { let dataPoints = [ - ChartDataPoint(value: 30, date: calendar.date(byAdding: .day, value: 0, to: weekOne)), - ChartDataPoint(value: 40, date: calendar.date(byAdding: .day, value: 1, to: weekOne)), - ChartDataPoint(value: 60, date: calendar.date(byAdding: .day, value: 3, to: weekOne)), - ChartDataPoint(value: 80, date: calendar.date(byAdding: .day, value: 5, to: weekOne)), - - ChartDataPoint(value: 40, date: calendar.date(byAdding: .day, value: 0, to: weekTwo)), - ChartDataPoint(value: 20, date: calendar.date(byAdding: .day, value: 2, to: weekTwo)), - ChartDataPoint(value: 70, date: calendar.date(byAdding: .day, value: 3, to: weekTwo)), - ChartDataPoint(value: 90, date: calendar.date(byAdding: .day, value: 5, to: weekTwo)), - - ChartDataPoint(value: 10, date: calendar.date(byAdding: .day, value: 1, to: weekThree)), - ChartDataPoint(value: 50, date: calendar.date(byAdding: .day, value: 2, to: weekThree)), - ChartDataPoint(value: 30, date: calendar.date(byAdding: .day, value: 4, to: weekThree)), - ChartDataPoint(value: 80, date: calendar.date(byAdding: .day, value: 5, to: weekThree)), - - ChartDataPoint(value: 60, date: calendar.date(byAdding: .day, value: 0, to: weekFour)), - ChartDataPoint(value: 40, date: calendar.date(byAdding: .day, value: 2, to: weekFour)), - ChartDataPoint(value: 80, date: calendar.date(byAdding: .day, value: 3, to: weekFour)), - ChartDataPoint(value: 40, date: calendar.date(byAdding: .day, value: 5, to: weekFour)) + LineChartDataPoint(value: 10), + LineChartDataPoint(value: 40), + LineChartDataPoint(value: 30), + LineChartDataPoint(value: 60) ] + let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) - guard let weeklyAverage = Calculations.weeklyAverage(dataPoints: dataPoints) else { - XCTFail("Failed") - return - } - - XCTAssertEqual(weeklyAverage[0].value, 52.5) - XCTAssertEqual(weeklyAverage[1].value, 55.0) - XCTAssertEqual(weeklyAverage[2].value, 42.5) - XCTAssertEqual(weeklyAverage[3].value, 55.0) - - XCTAssertEqual(weeklyAverage[0].xAxisLabel, formatterForXAxisLabel.string(from: weekOne)) - XCTAssertEqual(weeklyAverage[1].xAxisLabel, formatterForXAxisLabel.string(from: weekTwo)) - XCTAssertEqual(weeklyAverage[2].xAxisLabel, formatterForXAxisLabel.string(from: weekThree)) - XCTAssertEqual(weeklyAverage[3].xAxisLabel, formatterForXAxisLabel.string(from: weekFour)) - - XCTAssertEqual(weeklyAverage[0].pointDescription, formatterForPointLabel.string(from: weekOne)) - XCTAssertEqual(weeklyAverage[1].pointDescription, formatterForPointLabel.string(from: weekTwo)) - XCTAssertEqual(weeklyAverage[2].pointDescription, formatterForPointLabel.string(from: weekThree)) - XCTAssertEqual(weeklyAverage[3].pointDescription, formatterForPointLabel.string(from: weekFour)) + XCTAssertTrue(chartData.isGreaterThanTwo()) } - func testDailyAverage() { - let calendar = Calendar.current - - let formatterForXAxisLabel = DateFormatter() - formatterForXAxisLabel.locale = .current - formatterForXAxisLabel.setLocalizedDateFormatFromTemplate("d") - let formatterForPointLabel = DateFormatter() - formatterForPointLabel.locale = .current - formatterForPointLabel.setLocalizedDateFormatFromTemplate("dd MMMM YYYY") - - let components = DateComponents(year: 2021, month: 01, day: 03, hour: 10, minute: 0, second: 0) - - guard let date = calendar.date(from: components) else { - XCTFail("date failed") - return - } - guard let dayOne = calendar.date(byAdding: .day, value: 1, to: date) else { - XCTFail("monthOne failed") - return - } - guard let dayTwo = calendar.date(byAdding: .day, value: 2, to: date) else { - XCTFail("monthTwo failed") - return - } - guard let dayThree = calendar.date(byAdding: .day, value: 3, to: date) else { - XCTFail("monthThree failed") - return - } - guard let dayFour = calendar.date(byAdding: .day, value: 4, to: date) else { - XCTFail("monthFour failed") - return - } - + func testIsGreaterThanTwoFalse() { let dataPoints = [ - ChartDataPoint(value: 30, date: calendar.date(byAdding: .hour, value: 0, to: dayOne)), - ChartDataPoint(value: 40, date: calendar.date(byAdding: .hour, value: 1, to: dayOne)), - ChartDataPoint(value: 60, date: calendar.date(byAdding: .hour, value: 3, to: dayOne)), - ChartDataPoint(value: 80, date: calendar.date(byAdding: .hour, value: 5, to: dayOne)), - - ChartDataPoint(value: 40, date: calendar.date(byAdding: .hour, value: 0, to: dayTwo)), - ChartDataPoint(value: 20, date: calendar.date(byAdding: .hour, value: 2, to: dayTwo)), - ChartDataPoint(value: 70, date: calendar.date(byAdding: .hour, value: 3, to: dayTwo)), - ChartDataPoint(value: 90, date: calendar.date(byAdding: .hour, value: 5, to: dayTwo)), - - ChartDataPoint(value: 10, date: calendar.date(byAdding: .hour, value: 1, to: dayThree)), - ChartDataPoint(value: 50, date: calendar.date(byAdding: .hour, value: 2, to: dayThree)), - ChartDataPoint(value: 30, date: calendar.date(byAdding: .hour, value: 4, to: dayThree)), - ChartDataPoint(value: 80, date: calendar.date(byAdding: .hour, value: 5, to: dayThree)), - - ChartDataPoint(value: 60, date: calendar.date(byAdding: .hour, value: 0, to: dayFour)), - ChartDataPoint(value: 40, date: calendar.date(byAdding: .hour, value: 2, to: dayFour)), - ChartDataPoint(value: 80, date: calendar.date(byAdding: .hour, value: 3, to: dayFour)), - ChartDataPoint(value: 40, date: calendar.date(byAdding: .hour, value: 5, to: dayFour)) + LineChartDataPoint(value: 10), + LineChartDataPoint(value: 60) ] + let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) + XCTAssertFalse(chartData.isGreaterThanTwo()) + } + + func testMultiIsGreaterThanTwoTrue() { + let chartData = MultiLineChartData(dataSets: + MultiLineDataSet(dataSets: [ + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 10), + LineChartDataPoint(value: 40), + LineChartDataPoint(value: 30), + LineChartDataPoint(value: 60) + ], + legendTitle: "Bob"), + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 50), + LineChartDataPoint(value: 60), + LineChartDataPoint(value: 80), + LineChartDataPoint(value: 100) + ], + legendTitle: "Bob") + ])) - guard let dailyAverage = Calculations.dailyAverage(dataPoints: dataPoints) else { - XCTFail("Failed") - return - } - - XCTAssertEqual(dailyAverage[0].value, 52.5) - XCTAssertEqual(dailyAverage[1].value, 55.0) - XCTAssertEqual(dailyAverage[2].value, 42.5) - XCTAssertEqual(dailyAverage[3].value, 55.0) - - XCTAssertEqual(dailyAverage[0].xAxisLabel, formatterForXAxisLabel.string(from: dayOne)) - XCTAssertEqual(dailyAverage[1].xAxisLabel, formatterForXAxisLabel.string(from: dayTwo)) - XCTAssertEqual(dailyAverage[2].xAxisLabel, formatterForXAxisLabel.string(from: dayThree)) - XCTAssertEqual(dailyAverage[3].xAxisLabel, formatterForXAxisLabel.string(from: dayFour)) + XCTAssertTrue(chartData.isGreaterThanTwo()) + } + + func testMultiIsGreaterThanTwoFalse() { + let chartData = MultiLineChartData(dataSets: + MultiLineDataSet(dataSets: [ + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 10), + ], + legendTitle: "Bob"), + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 50) + ], + legendTitle: "Bob") + ])) - XCTAssertEqual(dailyAverage[0].pointDescription, formatterForPointLabel.string(from: dayOne)) - XCTAssertEqual(dailyAverage[1].pointDescription, formatterForPointLabel.string(from: dayTwo)) - XCTAssertEqual(dailyAverage[2].pointDescription, formatterForPointLabel.string(from: dayThree)) - XCTAssertEqual(dailyAverage[3].pointDescription, formatterForPointLabel.string(from: dayFour)) + XCTAssertFalse(chartData.isGreaterThanTwo()) } static var allTests = [ - // Chart Data + // Single Line Chart Data ("testMaxValue", testMaxValue), ("testMinValue", testMinValue), ("testAverage", testAverage), ("testRange", testRange), + ("testIsGreaterThanTwoTrue", testIsGreaterThanTwoTrue), + ("testIsGreaterThanTwoFalse", testIsGreaterThanTwoFalse), + // Multi Line Chart Data + ("testMultiLineMaxValue", testMultiLineMaxValue), + ("testMultiLineMinValue", testMultiLineMinValue), + ("testMultiLineAverage", testMultiLineAverage), + ("testMultiLineRange", testMultiLineRange), - // Calculations - ("testMonthlyAverage", testMonthlyAverage), - ("testWeeklyAverage", testWeeklyAverage), - ("testDailyAverage", testDailyAverage) + // Labels + ("testLineGetYLabels", testLineGetYLabels), + ("testBarGetYLabels", testBarGetYLabels), + + // Chart Data + ("testMultiIsGreaterThanTwoTrue", testIsGreaterThanTwoTrue), + ("testMultiIsGreaterThanTwoFalse", testIsGreaterThanTwoFalse), ] } From 84a0cc4a71a70eb1113fbe0b887163b04ee463d3 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 17 Feb 2021 18:14:15 +0000 Subject: [PATCH 059/152] Set access control. --- .../LineChart/Views/FilledLineChart.swift | 6 +- .../LineChart/Views/MultiLineChart.swift | 6 +- .../Views/SubViews/LineChartSubViews.swift | 92 ++++++++++++++----- .../Views/SubViews/PointsSubView.swift | 8 +- .../PieChart/Views/DoughnutChart.swift | 6 +- .../PieChart/Views/PieChart.swift | 6 +- .../Shared/Views/TouchOverlayBox.swift | 37 +++----- .../Views/HorizontalGridView.swift | 2 +- .../Views/VerticalGridView.swift | 8 +- 9 files changed, 100 insertions(+), 71 deletions(-) diff --git a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift index d342d503..f3f0ba5f 100644 --- a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift @@ -13,9 +13,7 @@ public struct FilledLineChart: View where ChartData: LineChartData { private let minValue : Double private let range : Double - - @State var startAnimation : Bool = false - + public init(chartData: ChartData) { self.chartData = chartData self.minValue = chartData.getMinValue() @@ -23,6 +21,8 @@ public struct FilledLineChart: View where ChartData: LineChartData { self.chartData.isFilled = true } + @State private var startAnimation : Bool = false + public var body: some View { if chartData.isGreaterThanTwo() { diff --git a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift index 420951ee..b9d4ddca 100644 --- a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift @@ -13,15 +13,15 @@ public struct MultiLineChart: View where ChartData: MultiLineChartDat private let minValue : Double private let range : Double - - @State var startAnimation : Bool = false - + public init(chartData: ChartData) { self.chartData = chartData self.minValue = chartData.getMinValue() self.range = chartData.getRange() } + @State private var startAnimation : Bool = false + public var body: some View { if chartData.isGreaterThanTwo() { diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift index 15e9dcef..a3d36ac9 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift @@ -9,14 +9,14 @@ import SwiftUI internal struct LineChartColourSubView: View where CD: LineChartDataProtocol { - let chartData : CD - let dataSet : LineDataSet - let minValue : Double - let range : Double - let colour : Color - let isFilled : Bool + private let chartData : CD + private let dataSet : LineDataSet + private let minValue : Double + private let range : Double + private let colour : Color + private let isFilled : Bool - @State var startAnimation : Bool = false + internal init(chartData : CD, dataSet : LineDataSet, @@ -33,6 +33,8 @@ internal struct LineChartColourSubView: View where CD: LineChartDataProtocol self.isFilled = isFilled } + @State private var startAnimation : Bool = false + internal var body: some View { LineShape(dataPoints: dataSet.dataPoints, @@ -61,18 +63,37 @@ internal struct LineChartColourSubView: View where CD: LineChartDataProtocol internal struct LineChartColoursSubView: View where CD: LineChartDataProtocol { - let chartData : CD - let dataSet : LineDataSet + private let chartData : CD + private let dataSet : LineDataSet - let minValue : Double - let range : Double - let colours : [Color] - let startPoint : UnitPoint - let endPoint : UnitPoint + private let minValue : Double + private let range : Double + private let colours : [Color] + private let startPoint : UnitPoint + private let endPoint : UnitPoint + + private let isFilled : Bool - let isFilled : Bool + internal init(chartData : CD, + dataSet : LineDataSet, + 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 var startAnimation : Bool = false + @State private var startAnimation : Bool = false internal var body: some View { @@ -110,19 +131,40 @@ internal struct LineChartColoursSubView: View where CD: LineChartDataProtoco } internal struct LineChartStopsSubView: View where CD: LineChartDataProtocol { + + - let chartData : CD - let dataSet : LineDataSet + private let chartData : CD + private let dataSet : LineDataSet - let minValue : Double - let range : Double - let stops : [Gradient.Stop] - let startPoint : UnitPoint - let endPoint : UnitPoint + 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 - let isFilled : Bool + internal init(chartData : CD, + dataSet : LineDataSet, + 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 var startAnimation : Bool = false + @State private var startAnimation : Bool = false internal var body: some View { diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift index 01e7e5b8..4206b75c 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift @@ -14,9 +14,7 @@ internal struct PointsSubView: View { private let range : Double private let animation: Animation private let isFilled : Bool - - @State var startAnimation : Bool = false - + internal init(dataSets : LineDataSet, minValue : Double, range : Double, @@ -30,6 +28,8 @@ internal struct PointsSubView: View { self.isFilled = isFilled } + @State private var startAnimation : Bool = false + internal var body: some View { switch dataSets.pointStyle.pointType { case .filled: @@ -94,9 +94,7 @@ internal struct PointsSubView: View { .animateOnDisappear(using: animation) { self.startAnimation = false } - } } - } diff --git a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift index 7bf0f94a..a174a210 100644 --- a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift @@ -10,13 +10,13 @@ import SwiftUI public struct DoughnutChart: View where ChartData: DoughnutChartData { @ObservedObject var chartData: ChartData - - @State var startAnimation : Bool = false - + 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 diff --git a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift index 811890f4..12337459 100644 --- a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift @@ -10,13 +10,13 @@ import SwiftUI public struct PieChart: View where ChartData: PieChartData { @ObservedObject var chartData: ChartData - - @State var startAnimation : Bool = false - + 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 diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index ea02f17d..fea57195 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -61,36 +61,21 @@ internal struct TouchOverlayBox: View { .background( GeometryReader { geo in if isTouchCurrent { - 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 + RoundedRectangle(cornerRadius: 5.0, style: .continuous) + .fill(Color.systemsBackground) + } + .overlay( + Group { + RoundedRectangle(cornerRadius: 5.0) + .stroke(Color.primary, lineWidth: 1) + } + ) + .onChange(of: geo.frame(in: .local)) { frame in + self.boxFrame = frame } - ) - .onChange(of: geo.frame(in: .local)) { frame in - self.boxFrame = frame } } - } ) } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift index 2581574d..f906f106 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift @@ -11,7 +11,7 @@ internal struct HorizontalGridView: View where T: LineAndBarChartData { var chartData : T - @State var startAnimation : Bool = false + @State private var startAnimation : Bool = false var body: some View { HorizontalGridShape() diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift index 5be1ae5f..3414e1b4 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift @@ -9,9 +9,13 @@ import SwiftUI internal struct VerticalGridView: View where T: LineAndBarChartData { - var chartData : T + @ObservedObject private var chartData : T - @State var startAnimation : Bool = false + internal init(chartData: T) { + self.chartData = chartData + } + + @State private var startAnimation : Bool = false var body: some View { VerticalGridShape() From e6c0efeeddfeed307f1523b664f64978d935a648 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 17 Feb 2021 18:27:24 +0000 Subject: [PATCH 060/152] Add animations. --- .../SharedLineAndBar/ViewModifiers/YAxisPOI.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index 3981f76a..e53de26f 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -52,6 +52,8 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { self.minValue = chartData.getMinValue() } + @State private var startAnimation : Bool = false + internal func body(content: Content) -> some View { ZStack { if chartData.isGreaterThanTwo() { @@ -60,6 +62,12 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { valueLabel } else { content } } + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = false + } .onAppear { if !chartData.legends.contains(where: { $0.legend == markerName }) { // init twice chartData.legends.append(LegendData(id : uuid, @@ -78,6 +86,7 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { minValue : minValue, maxValue : maxValue, chartType : chartData.chartType.chartType) + .trim(to: startAnimation ? 1 : 0) .stroke(lineColour, style: strokeStyle) } @@ -120,8 +129,10 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { .overlay(DiamondShape() .stroke(lineColour, style: strokeStyle) ) - .position(x: geo.size.width / 2, + .position(x: startAnimation ? geo.size.width / 2 : 0, y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo)) + .opacity(startAnimation ? 1 : 0) + .animation(chartData.chartStyle.globalAnimation.speed(2)) } } } From 653c7e36d7223ca89042abed01332375c1153df2 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 18 Feb 2021 10:44:42 +0000 Subject: [PATCH 061/152] Re-organise how CTStyle is implemented. --- .../BarChart/Models/ChartData/BarChartData.swift | 9 +++++++++ .../Models/ChartData/GroupedBarChartData.swift | 9 +++++++++ .../Models/ChartData/StackedBarChartData.swift | 9 +++++++++ .../Models/Protocols/BarChartProtocols.swift | 7 +++---- .../Protocols/BarChartProtocolsExtensions.swift | 9 +-------- .../Models/ChartData/LineChartData.swift | 13 +++++++++++++ .../Models/ChartData/MultiLineChartData.swift | 13 +++++++++++++ .../Models/Protocols/LineChartProtocols.swift | 15 ++++++--------- .../Protocols/LineChartProtocolsExtensions.swift | 16 ---------------- .../PieChart/Models/PieChartProtocols.swift | 12 ++++++------ .../Models/Protocols/LineAndBarProtocols.swift | 13 +++++++++++-- .../ViewModifiers/AxisBorders.swift | 1 + 12 files changed, 81 insertions(+), 45 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 9d325018..e4940d81 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -251,6 +251,14 @@ public final class BarChartData: BarChartDataProtocol { } } } + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let maxValue: Double = self.getMaxValue() + for index in 0...self.chartStyle.yAxisNumberOfLabels { + labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) + } + return labels + } // MARK: - Touch public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { @@ -356,4 +364,5 @@ public final class BarChartData: BarChartDataProtocol { public typealias Set = BarDataSet public typealias DataPoint = BarChartDataPoint + public typealias CTStyle = BarChartStyle } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 5de406de..9dcf8cd7 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -269,6 +269,15 @@ public final class GroupedBarChartData: BarChartDataProtocol { } } + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let maxValue: Double = self.getMaxValue() + for index in 0...self.chartStyle.yAxisNumberOfLabels { + labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) + } + return labels + } + // MARK: - Touch public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [GroupedBarChartDataPoint] { diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index d613f7b1..5c0efde9 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -79,6 +79,15 @@ public final class StackedBarChartData: BarChartDataProtocol { } } + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let maxValue: Double = self.getMaxValue() + for index in 0...self.chartStyle.yAxisNumberOfLabels { + labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) + } + return labels + } + // MARK: - Touch public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [GroupedBarChartDataPoint] { diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index b1454a6d..09bef606 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -19,7 +19,8 @@ import SwiftUI - Tag: BarChartDataProtocol */ -public protocol BarChartDataProtocol: LineAndBarChartData where CTStyle: CTBarChartStyle { +public protocol BarChartDataProtocol: LineAndBarChartData { + var barStyle : BarStyle { get set } /** Data model conatining the style data for the chart. @@ -27,11 +28,9 @@ public protocol BarChartDataProtocol: LineAndBarChartData where CTStyle: CTBarCh # Reference [CTChartStyle](x-source-tag://CTChartStyle) */ - var chartStyle : CTStyle { get set } +// var chartStyle : CTStyle { get set } } -//public protocol GroupedBarChartDataProtocol: BarChartDataProtocol {} - diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift index 8cc76b36..45e44bd8 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift @@ -8,12 +8,5 @@ import Foundation extension LineAndBarChartData where Self: BarChartDataProtocol { - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let maxValue: Double = self.getMaxValue() - for index in 0...self.chartStyle.yAxisNumberOfLabels { - labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) - } - return labels - } + } diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index 38328bff..ca470582 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -241,6 +241,19 @@ public final class LineChartData: LineChartDataProtocol { } } + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let dataRange : Double = self.getRange() + let minValue : Double = self.getMinValue() + let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) + + labels.append(minValue) + for index in 1...self.chartStyle.yAxisNumberOfLabels { + labels.append(minValue + range * Double(index)) + } + return labels + } + // MARK: - Touch public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [LineChartDataPoint] { var points : [LineChartDataPoint] = [] diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index 1e76997b..45ac8387 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -254,6 +254,19 @@ public final class MultiLineChartData: LineChartDataProtocol { } } + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let dataRange : Double = self.getRange() + let minValue : Double = self.getMinValue() + let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) + + labels.append(minValue) + for index in 1...self.chartStyle.yAxisNumberOfLabels { + labels.append(minValue + range * Double(index)) + } + return labels + } + // MARK: - Touch public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [LineChartDataPoint] { var points : [LineChartDataPoint] = [] diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index 388423f7..7e461470 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -18,15 +18,8 @@ import SwiftUI - Tag: LineChartDataProtocol */ -public protocol LineChartDataProtocol: LineAndBarChartData where CTStyle: CTLineChartStyle { - /** - Data model conatining the style data for the chart. - - # Reference - [CTChartStyle](x-source-tag://CTChartStyle) - */ - var chartStyle : CTStyle { get set } - +public protocol LineChartDataProtocol: LineAndBarChartData { + /** Whether it is a normal or filled line. */ @@ -45,6 +38,8 @@ public protocol LineChartDataProtocol: LineAndBarChartData where CTStyle: CTLine func getIndicatorLocation(rect: CGRect, dataSet: LineDataSet, touchLocation: CGPoint) -> CGPoint } + + // MARK: - Style /** A protocol to extend functionality of `CTLineAndBarChartStyle` specifically for Line Charts. @@ -66,6 +61,8 @@ public protocol CTLineChartStyle : CTLineAndBarChartStyle { } + + // MARK: - DataSet /** A protocol to extend functionality of `SingleDataSet` specifically for Line Charts. diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index 6963f90d..936255a2 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -7,22 +7,6 @@ import SwiftUI -// MARK: Labels -extension LineAndBarChartData where Self: LineChartDataProtocol { - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let dataRange : Double = self.getRange() - let minValue : Double = self.getMinValue() - let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) - - labels.append(minValue) - for index in 1...self.chartStyle.yAxisNumberOfLabels { - labels.append(minValue + range * Double(index)) - } - return labels - } -} - // MARK: - Position Indicator extension LineChartDataProtocol { diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift index fd283255..8eafd5c1 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift @@ -17,7 +17,7 @@ import SwiftUI - Tag: PieAndDoughnutChartDataProtocol */ -public protocol PieAndDoughnutChartDataProtocol: ChartData where CTStyle : CTPieAndDoughnutChartStyle{ +public protocol PieAndDoughnutChartDataProtocol: ChartData { /** Data model conatining the style data for the chart. @@ -25,7 +25,7 @@ public protocol PieAndDoughnutChartDataProtocol: ChartData where CTStyle : CTPie # Reference [CTChartStyle](x-source-tag://CTChartStyle) */ - var chartStyle : CTStyle { get set } +// var chartStyle : CTStyle { get set } } /** @@ -36,7 +36,7 @@ public protocol PieAndDoughnutChartDataProtocol: ChartData where CTStyle : CTPie - Tag: PieChartDataProtocol */ -public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol where CTStyle: CTPieChartStyle { +public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol { /** Data model conatining the style data for the chart. @@ -44,7 +44,7 @@ public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol where CTS # Reference [CTChartStyle](x-source-tag://CTChartStyle) */ - var chartStyle : CTStyle { get set } +// var chartStyle : CTStyle { get set } } /** @@ -55,7 +55,7 @@ public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol where CTS - Tag: DoughnutChartDataProtocol */ -public protocol DoughnutChartDataProtocol : PieAndDoughnutChartDataProtocol where CTStyle: CTDoughnutChartStyle { +public protocol DoughnutChartDataProtocol : PieAndDoughnutChartDataProtocol { /** Data model conatining the style data for the chart. @@ -63,7 +63,7 @@ public protocol DoughnutChartDataProtocol : PieAndDoughnutChartDataProtocol wher # Reference [CTChartStyle](x-source-tag://CTChartStyle) */ - var chartStyle : CTStyle { get set } +// var chartStyle : CTStyle { get set } } public protocol CTMultiPieChartDataPoints: CTChartDataPoint {} diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index ca527ef4..7a84cc52 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -16,11 +16,11 @@ import SwiftUI - Tag: LineAndBarChartData */ -public protocol LineAndBarChartData : ChartData where CTStyle: CTLineAndBarChartStyle { +public protocol LineAndBarChartData : ChartData { /// Apple's `associatedtype` for outputting `some View`. associatedtype Body : View - + associatedtype CTLineAndBarCS : CTLineAndBarChartStyle /** Array of strings for the labels on the X Axis instead of the labels in the data points. @@ -37,6 +37,15 @@ public protocol LineAndBarChartData : ChartData where CTStyle: CTLineAndBarChart */ var viewData: ChartViewData { get set } + + /** + Data model conatining the style data for the chart. + + # Reference + [CTChartStyle](x-source-tag://CTChartStyle) + */ + var chartStyle: CTLineAndBarCS { get set } + /** Displays a view for the labels on the X Axis. diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift index 7ea28a92..7ac4d99b 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift @@ -15,6 +15,7 @@ internal struct XAxisBorder: ViewModifier where T: LineAndBarChartData { init(chartData: T) { self.chartData = chartData + self.labelsAndTop = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .top self.labelsAndBottom = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .bottom } From 1a1f9f7c6b500bf7850d61faec4ed49b90f99a39 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 18 Feb 2021 11:19:48 +0000 Subject: [PATCH 062/152] Make Body available from ChartData. --- .../Models/ChartData/BarChartData.swift | 6 ++-- .../Models/Protocols/BarChartProtocols.swift | 7 ---- .../Models/Doughnut/DoughnutChartData.swift | 7 ++-- .../PieChart/Models/Pie/PieChartData.swift | 4 ++- .../PieChart/Models/PieChartProtocols.swift | 36 +++---------------- .../Models/Protocols/SharedProtocols.swift | 14 ++++++++ .../Protocols/LineAndBarProtocols.swift | 18 ++-------- 7 files changed, 32 insertions(+), 60 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index e4940d81..f085cdfb 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -362,7 +362,7 @@ public final class BarChartData: BarChartDataProtocol { // } } - public typealias Set = BarDataSet - public typealias DataPoint = BarChartDataPoint - public typealias CTStyle = BarChartStyle + public typealias Set = BarDataSet + public typealias DataPoint = BarChartDataPoint + public typealias CTStyle = BarChartStyle } diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index 09bef606..f348838b 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -22,13 +22,6 @@ import SwiftUI public protocol BarChartDataProtocol: LineAndBarChartData { var barStyle : BarStyle { get set } - /** - Data model conatining the style data for the chart. - - # Reference - [CTChartStyle](x-source-tag://CTChartStyle) - */ -// var chartStyle : CTStyle { get set } } diff --git a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift index cddbcdb2..071ca417 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift @@ -36,7 +36,10 @@ public final class DoughnutChartData: DoughnutChartDataProtocol { self.makeDataPoints() } + + public func getXAxisLabels() -> some View { EmptyView() } - public typealias Set = PieDataSet - public typealias DataPoint = PieChartDataPoint + public typealias Set = PieDataSet + public typealias DataPoint = PieChartDataPoint + public typealias CTStyle = DoughnutChartStyle } diff --git a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift index 2d2ca48f..e63da877 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift @@ -37,8 +37,10 @@ public final class PieChartData: PieChartDataProtocol { self.makeDataPoints() } + public func getXAxisLabels() -> some View { EmptyView() } - public typealias Set = PieDataSet + public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint + public typealias CTStyle = PieChartStyle } diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift index 8eafd5c1..19b94e4f 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift @@ -7,7 +7,6 @@ import SwiftUI - // MARK: - Chart Data /** A protocol to extend functionality of `ChartData` specifically for Pie and Doughnut Charts. @@ -17,16 +16,7 @@ import SwiftUI - Tag: PieAndDoughnutChartDataProtocol */ -public protocol PieAndDoughnutChartDataProtocol: ChartData { - - /** - Data model conatining the style data for the chart. - - # Reference - [CTChartStyle](x-source-tag://CTChartStyle) - */ -// var chartStyle : CTStyle { get set } -} +public protocol PieAndDoughnutChartDataProtocol: ChartData {} /** A protocol to extend functionality of `PieAndDoughnutChartDataProtocol` specifically for Pie Charts. @@ -36,16 +26,7 @@ public protocol PieAndDoughnutChartDataProtocol: ChartData { - Tag: PieChartDataProtocol */ -public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol { - - /** - Data model conatining the style data for the chart. - - # Reference - [CTChartStyle](x-source-tag://CTChartStyle) - */ -// var chartStyle : CTStyle { get set } -} +public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol {} /** A protocol to extend functionality of `PieAndDoughnutChartDataProtocol` specifically for Doughnut Charts. @@ -55,16 +36,9 @@ public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol { - Tag: DoughnutChartDataProtocol */ -public protocol DoughnutChartDataProtocol : PieAndDoughnutChartDataProtocol { - - /** - Data model conatining the style data for the chart. - - # Reference - [CTChartStyle](x-source-tag://CTChartStyle) - */ -// var chartStyle : CTStyle { get set } -} +public protocol DoughnutChartDataProtocol : PieAndDoughnutChartDataProtocol {} + + public protocol CTMultiPieChartDataPoints: CTChartDataPoint {} diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index dc27c7a3..4169e643 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -21,6 +21,8 @@ public protocol ChartData: ObservableObject, Identifiable { associatedtype DataPoint: CTChartDataPoint associatedtype CTStyle : CTChartStyle + associatedtype Body : View + var id: ID { get } /** @@ -135,6 +137,18 @@ public protocol ChartData: ObservableObject, Identifiable { - Tag: getDataPoint */ func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] + + /** + Displays a view for the labels on the X Axis. + + Labels can come from either [CTChartDataPoint](x-source-tag://CTChartDataPoint) + or [ChartData](x-source-tag://ChartData) + + - Returns: An `HStack` of `Text` containin x axis labels. + + - Tag: getXAxidLabels + */ + func getXAxisLabels() -> Body } // MARK: - Data Sets diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index 7a84cc52..8a612207 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -17,9 +17,7 @@ import SwiftUI - Tag: LineAndBarChartData */ public protocol LineAndBarChartData : ChartData { - - /// Apple's `associatedtype` for outputting `some View`. - associatedtype Body : View + associatedtype CTLineAndBarCS : CTLineAndBarChartStyle /** Array of strings for the labels on the X Axis instead of the labels in the data points. @@ -45,19 +43,7 @@ public protocol LineAndBarChartData : ChartData { [CTChartStyle](x-source-tag://CTChartStyle) */ var chartStyle: CTLineAndBarCS { get set } - - /** - Displays a view for the labels on the X Axis. - - Labels can come from either [CTChartDataPoint](x-source-tag://CTChartDataPoint) - or [ChartData](x-source-tag://ChartData) - - - Returns: An `HStack` of `Text` containin x axis labels. - - - Tag: getXAxidLabels - */ - func getXAxisLabels() -> Body - + /** Labels to display on the Y axis From 7c29919d6141d2ac4131443d03064cfc79afab9a Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 18 Feb 2021 11:59:41 +0000 Subject: [PATCH 063/152] Bring legends back. --- .../Models/ChartData/BarChartData.swift | 151 +++++++++--------- 1 file changed, 76 insertions(+), 75 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index f085cdfb..b03b834a 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -285,81 +285,82 @@ public final class BarChartData: BarChartDataProtocol { // MARK: - Legends public func setupLegends() { -// switch self.dataSets.style.colourFrom { -// case .barStyle: -// if dataSets.style.colourType == .colour, -// let colour = dataSets.style.colour -// { -// self.legends.append(LegendData(id : dataSets.id, -// legend : dataSets.legendTitle, -// colour : colour, -// strokeStyle: nil, -// prioity : 1, -// chartType : .bar)) -// } else if dataSets.style.colourType == .gradientColour, -// let colours = dataSets.style.colours -// { -// self.legends.append(LegendData(id : dataSets.id, -// legend : dataSets.legendTitle, -// colours : colours, -// startPoint : .leading, -// endPoint : .trailing, -// strokeStyle: nil, -// prioity : 1, -// chartType : .bar)) -// } else if dataSets.style.colourType == .gradientStops, -// let stops = dataSets.style.stops -// { -// self.legends.append(LegendData(id : dataSets.id, -// legend : dataSets.legendTitle, -// stops : stops, -// startPoint : .leading, -// endPoint : .trailing, -// strokeStyle: nil, -// prioity : 1, -// chartType : .bar)) -// } -// case .dataPoints: -// -// for data in dataSets.dataPoints { -// -// if data.colourType == .colour, -// let colour = data.colour, -// let legend = data.pointDescription -// { -// self.legends.append(LegendData(id : data.id, -// legend : legend, -// colour : colour, -// strokeStyle: nil, -// prioity : 1, -// chartType : .bar)) -// } else if data.colourType == .gradientColour, -// let colours = data.colours, -// let legend = data.pointDescription -// { -// self.legends.append(LegendData(id : data.id, -// legend : legend, -// colours : colours, -// startPoint : .leading, -// endPoint : .trailing, -// strokeStyle: nil, -// prioity : 1, -// chartType : .bar)) -// } else if data.colourType == .gradientStops, -// let stops = data.stops, -// let legend = data.pointDescription -// { -// self.legends.append(LegendData(id : data.id, -// legend : legend, -// stops : stops, -// startPoint : .leading, -// endPoint : .trailing, -// strokeStyle: nil, -// prioity : 1, -// chartType : .bar)) -// } -// } -// } + + switch self.barStyle.colourFrom { + case .barStyle: + if self.barStyle.colourType == .colour, + let colour = self.barStyle.colour + { + self.legends.append(LegendData(id : dataSets.id, + legend : dataSets.legendTitle, + colour : colour, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if self.barStyle.colourType == .gradientColour, + let colours = self.barStyle.colours + { + self.legends.append(LegendData(id : dataSets.id, + legend : dataSets.legendTitle, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if self.barStyle.colourType == .gradientStops, + let stops = self.barStyle.stops + { + self.legends.append(LegendData(id : dataSets.id, + legend : dataSets.legendTitle, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } + case .dataPoints: + + for data in dataSets.dataPoints { + + if data.colourType == .colour, + let colour = data.colour, + let legend = data.pointDescription + { + self.legends.append(LegendData(id : data.id, + legend : legend, + colour : colour, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if data.colourType == .gradientColour, + let colours = data.colours, + let legend = data.pointDescription + { + self.legends.append(LegendData(id : data.id, + legend : legend, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if data.colourType == .gradientStops, + let stops = data.stops, + let legend = data.pointDescription + { + self.legends.append(LegendData(id : data.id, + legend : legend, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } + } + } } public typealias Set = BarDataSet From b375b62b1b61e91c6ebe2a5790a263e37ac183f5 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 18 Feb 2021 12:00:43 +0000 Subject: [PATCH 064/152] Re-organise init. --- .../BarChart/Models/ChartData/GroupedBarChartData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 9dcf8cd7..b573a54f 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -205,11 +205,11 @@ public final class GroupedBarChartData: BarChartDataProtocol { /// - 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 : GroupedBarDataSets, + groupLegends: [GroupedBarLegend], metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, barStyle : BarStyle = BarStyle(), chartStyle : BarChartStyle = BarChartStyle(), - groupLegends: [GroupedBarLegend], noDataText : Text = Text("No Data") ) { self.dataSets = dataSets From 58b80bbaaf8cc24135dcb83e7de3d4d3cba179f0 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 18 Feb 2021 12:01:10 +0000 Subject: [PATCH 065/152] Set up legends. --- .../ChartData/StackedBarChartData.swift | 86 +++---------------- 1 file changed, 11 insertions(+), 75 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index 5c0efde9..27e7b301 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -21,10 +21,12 @@ public final class StackedBarChartData: BarChartDataProtocol { @Published public var viewData : ChartViewData @Published public var infoView : InfoViewData = InfoViewData() + public var groupLegends : [GroupedBarLegend] public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) public init(dataSets : GroupedBarDataSets, + groupLegends: [GroupedBarLegend], metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, barStyle : BarStyle = BarStyle(), @@ -36,6 +38,7 @@ public final class StackedBarChartData: BarChartDataProtocol { self.xAxisLabels = xAxisLabels self.barStyle = barStyle self.chartStyle = chartStyle + self.groupLegends = groupLegends self.noDataText = noDataText self.legends = [LegendData]() self.viewData = ChartViewData() @@ -142,81 +145,14 @@ public final class StackedBarChartData: BarChartDataProtocol { // MARK: - Legends public func setupLegends() { -// switch dataSets.dataSets[0].style.colourFrom { -// case .barStyle: -// if dataSets.dataSets[0].style.colourType == .colour, -// let colour = dataSets.dataSets[0].style.colour -// { -// self.legends.append(LegendData(id : dataSets.dataSets[0].id, -// legend : dataSets.dataSets[0].legendTitle, -// colour : colour, -// strokeStyle: nil, -// prioity : 1, -// chartType : .bar)) -// } else if dataSets.dataSets[0].style.colourType == .gradientColour, -// let colours = dataSets.dataSets[0].style.colours -// { -// self.legends.append(LegendData(id : dataSets.dataSets[0].id, -// legend : dataSets.dataSets[0].legendTitle, -// colours : colours, -// startPoint : .leading, -// endPoint : .trailing, -// strokeStyle: nil, -// prioity : 1, -// chartType : .bar)) -// } else if dataSets.dataSets[0].style.colourType == .gradientStops, -// let stops = dataSets.dataSets[0].style.stops -// { -// self.legends.append(LegendData(id : dataSets.dataSets[0].id, -// legend : dataSets.dataSets[0].legendTitle, -// stops : stops, -// startPoint : .leading, -// endPoint : .trailing, -// strokeStyle: nil, -// prioity : 1, -// chartType : .bar)) -// } -// case .dataPoints: -// -// for data in dataSets.dataSets[0].dataPoints { -// -// if data.colourType == .colour, -// let colour = data.colour, -// let legend = data.pointDescription -// { -// self.legends.append(LegendData(id : data.id, -// legend : legend, -// colour : colour, -// strokeStyle: nil, -// prioity : 1, -// chartType : .bar)) -// } else if data.colourType == .gradientColour, -// let colours = data.colours, -// let legend = data.pointDescription -// { -// self.legends.append(LegendData(id : data.id, -// legend : legend, -// colours : colours, -// startPoint : .leading, -// endPoint : .trailing, -// strokeStyle: nil, -// prioity : 1, -// chartType : .bar)) -// } else if data.colourType == .gradientStops, -// let stops = data.stops, -// let legend = data.pointDescription -// { -// self.legends.append(LegendData(id : data.id, -// legend : legend, -// stops : stops, -// startPoint : .leading, -// endPoint : .trailing, -// strokeStyle: nil, -// prioity : 1, -// chartType : .bar)) -// } -// } -// } + for legend in self.groupLegends { + self.legends.append(LegendData(id: UUID(), + legend: legend.title, + colour: legend.colour, + strokeStyle: nil, + prioity: 1, + chartType: .bar)) + } } public typealias Set = GroupedBarDataSets public typealias DataPoint = GroupedBarChartDataPoint From c0afc2800958972f9c8e9d1572813cbffbbe84b4 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 18 Feb 2021 12:01:59 +0000 Subject: [PATCH 066/152] Catch error if bar has no colour data. --- .../BarChart/Views/SubViews/BarChartSubViews.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift index 474dc4a7..9e310367 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift @@ -79,6 +79,8 @@ internal struct BarChartDataPointSubView: View { let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + } else { + ColourBar(.blue, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) } } From c3bdf441f6012040a5de3feaf3b2003acb6ecda9 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 18 Feb 2021 12:02:23 +0000 Subject: [PATCH 067/152] Tidy up. --- .../Protocols/BarChartProtocolsExtensions.swift | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift deleted file mode 100644 index 45e44bd8..00000000 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// BarChartProtocolsExtensions.swift -// -// -// Created by Will Dale on 13/02/2021. -// - -import Foundation - -extension LineAndBarChartData where Self: BarChartDataProtocol { - -} From e8a7081522466b24cd0eedfc932d1b79f47f7411 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 18 Feb 2021 12:02:54 +0000 Subject: [PATCH 068/152] Bring stacked bar chart back. --- .../BarChart/Views/StackedBarChart.swift | 199 +++++++++--------- 1 file changed, 99 insertions(+), 100 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift index 0a1d030c..73a4fcfb 100644 --- a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift @@ -1,103 +1,102 @@ -//// -//// StackedBarChart.swift -//// -//// -//// Created by Will Dale on 12/02/2021. -//// // -//import SwiftUI +// StackedBarChart.swift +// // +// Created by Will Dale on 12/02/2021. // -//public struct StackedBarChart: View where ChartData: StackedBarChartData { -// -// @ObservedObject var chartData: ChartData -// -// 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 -// -// MultiPartBarSubView(dataSet: dataSet) -// .scaleEffect(y: startAnimation ? CGFloat(DataFunctions.dataSetMaxValue(from: dataSet) / chartData.getMaxValue()) : 0, anchor: .bottom) -// .scaleEffect(x: chartData.barStyle.barWidth, anchor: .center) -// .animateOnAppear(using: chartData.chartStyle.globalAnimation) { -// self.startAnimation = true -// } -// .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { -// self.startAnimation = false -// } -// } -// } -// -// } else { CustomNoDataView(chartData: chartData) } -// } -//} -// -///** -// -// */ -//internal struct MultiPartBarSubView: View { -// -// private let dataSet : MultiBarDataSet -// -// internal init(dataSet: MultiBarDataSet) { -// self.dataSet = dataSet -// } -// -// internal var body: some View { -// GeometryReader { geo in -// -// VStack(spacing: 0) { -// ForEach(dataSet.dataPoints.reversed()) { dataPoint in -// -// if dataPoint.colourType == .colour, -// let colour = dataPoint.colour -// { -// -// ColourPartBar(colour, getHeight(height : geo.size.height, -// dataSet : dataSet, -// dataPoint : dataPoint)) -// -// } else if dataPoint.colourType == .gradientColour, -// let colours = dataPoint.colours, -// let startPoint = dataPoint.startPoint, -// let endPoint = dataPoint.endPoint -// { -// -// GradientColoursPartBar(colours, startPoint, endPoint, getHeight(height: geo.size.height, -// dataSet : dataSet, -// dataPoint : dataPoint)) -// -// } else if dataPoint.colourType == .gradientStops, -// let stops = dataPoint.stops, -// let startPoint = dataPoint.startPoint, -// let endPoint = dataPoint.endPoint -// { -// -// let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) -// -// GradientStopsPartBar(safeStops, startPoint, endPoint, getHeight(height: geo.size.height, -// dataSet : dataSet, -// dataPoint : dataPoint)) -// } -// -// } -// } -// } -// } -// -// -// private func getHeight(height: CGFloat, dataSet: MultiBarDataSet, dataPoint: MultiPartBarChartDataPoint) -> CGFloat { -// let value = dataPoint.value -// let sum = dataSet.dataPoints.reduce(0) { $0 + $1.value } -// return height * CGFloat(value / sum) -// } -//} + +import SwiftUI + +public struct StackedBarChart: View where ChartData: StackedBarChartData { + + @ObservedObject var chartData: ChartData + + 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 + + MultiPartBarSubView(dataSet: dataSet) + .scaleEffect(y: startAnimation ? CGFloat(DataFunctions.dataSetMaxValue(from: dataSet) / chartData.getMaxValue()) : 0, anchor: .bottom) + .scaleEffect(x: chartData.barStyle.barWidth, anchor: .center) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = false + } + } + } + + } else { CustomNoDataView(chartData: chartData) } + } +} + +/** + + */ +internal struct MultiPartBarSubView: View { + + private let dataSet : GroupedBarDataSet + + internal init(dataSet: GroupedBarDataSet) { + self.dataSet = dataSet + } + + internal var body: some View { + GeometryReader { geo in + + VStack(spacing: 0) { + ForEach(dataSet.dataPoints.reversed()) { dataPoint in + + if dataPoint.colourType == .colour, + let colour = dataPoint.colour + { + + ColourPartBar(colour, getHeight(height : geo.size.height, + dataSet : dataSet, + dataPoint : dataPoint)) + + } else if dataPoint.colourType == .gradientColour, + let colours = dataPoint.colours, + let startPoint = dataPoint.startPoint, + let endPoint = dataPoint.endPoint + { + + GradientColoursPartBar(colours, startPoint, endPoint, getHeight(height: geo.size.height, + dataSet : dataSet, + dataPoint : dataPoint)) + + } else if dataPoint.colourType == .gradientStops, + let stops = dataPoint.stops, + let startPoint = dataPoint.startPoint, + let endPoint = dataPoint.endPoint + { + + let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) + + GradientStopsPartBar(safeStops, startPoint, endPoint, getHeight(height: geo.size.height, + dataSet : dataSet, + dataPoint : dataPoint)) + } + + } + } + } + } + + + private func getHeight(height: CGFloat, dataSet: GroupedBarDataSet, dataPoint: GroupedBarChartDataPoint) -> CGFloat { + let value = dataPoint.value + let sum = dataSet.dataPoints.reduce(0) { $0 + $1.value } + return height * CGFloat(value / sum) + } +} From 35c11d41e4c277edb2ee59af292611f0c46a96a9 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 18 Feb 2021 14:05:54 +0000 Subject: [PATCH 069/152] Move Touch interaction into protocol. --- .../Models/ChartData/BarChartData.swift | 33 +++ .../ChartData/GroupedBarChartData.swift | 33 +++ .../ChartData/StackedBarChartData.swift | 78 ++++++- .../Models/ChartData/LineChartData.swift | 47 +++++ .../Models/ChartData/MultiLineChartData.swift | 51 ++++- .../LineChartProtocolsExtensions.swift | 90 ++++---- .../Models/Doughnut/DoughnutChartData.swift | 1 + .../PieChart/Models/Pie/PieChartData.swift | 1 + .../Models/Protocols/SharedProtocols.swift | 9 +- .../Shared/ViewModifiers/TouchOverlay.swift | 195 +----------------- 10 files changed, 297 insertions(+), 241 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index b03b834a..304708c6 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -283,6 +283,39 @@ public final class BarChartData: BarChartDataProtocol { return locations } + public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { + let positions = self.getPointLocation(touchLocation: touchLocation, + chartSize: chartSize) + return ZStack { + ForEach(positions, id: \.self) { position in + + switch self.chartStyle.markerType { + case .vertical: + MarkerFull(position: position) + .stroke(Color.primary, lineWidth: 2) + case .rectangle: + 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 public func setupLegends() { diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index b573a54f..3d546b6c 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -339,6 +339,39 @@ public final class GroupedBarChartData: BarChartDataProtocol { return locations } + public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { + let positions = self.getPointLocation(touchLocation: touchLocation, + chartSize: chartSize) + return ZStack { + ForEach(positions, id: \.self) { position in + + switch self.chartStyle.markerType { + case .vertical: + MarkerFull(position: position) + .stroke(Color.primary, lineWidth: 2) + case .rectangle: + 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 public func setupLegends() { diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index 27e7b301..14523f19 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -139,10 +139,86 @@ public final class StackedBarChartData: BarChartDataProtocol { } public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - let locations : [HashablePoint] = [] + var locations : [HashablePoint] = [] + + // Filter to get the right dataset based on the x axis. + let superXSection : CGFloat = chartSize.size.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.getMaxValue() + 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.size.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.size.height) }) + + if let index = yIndex?.offset { + if index >= 0 && index < dataSet.dataPoints.count { + + locations.append(HashablePoint(x: (CGFloat(superIndex) * superXSection) + (superXSection / 2), + y: (chartSize.size.height - endPointOfElements[index]))) + } + } + } + return locations } + public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { + let positions = self.getPointLocation(touchLocation: touchLocation, + chartSize: chartSize) + return ZStack { + ForEach(positions, id: \.self) { position in + + switch self.chartStyle.markerType { + case .vertical: + MarkerFull(position: position) + .stroke(Color.primary, lineWidth: 2) + case .rectangle: + 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 public func setupLegends() { for legend in self.groupLegends { diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index ca470582..7c0fae40 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -281,6 +281,53 @@ public final class LineChartData: LineChartDataProtocol { } return locations } + + public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { + let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + dataSet: dataSets, + touchLocation: touchLocation) + return ZStack { + switch self.chartStyle.markerType { + case .vertical: + Vertical(position: position) + .stroke(Color.primary, lineWidth: 2) + case .rectangle: + RoundedRectangle(cornerRadius: 25.0, style: .continuous) + .fill(Color.clear) + .frame(width: 100, height: chartSize.frame(in: .local).height) + .position(x: position.x, + y: chartSize.frame(in: .local).midY) + .overlay( + RoundedRectangle(cornerRadius: 25.0, style: .continuous) + .stroke(Color.primary, lineWidth: 2) + .shadow(color: .primary, radius: 4, x: 0, y: 0) + .frame(width: 50, height: chartSize.frame(in: .local).height) + .position(x: position.x, + y: chartSize.frame(in: .local).midY) + ) + 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) + } + + PosistionIndicator() + .frame(width: 15, height: 15) + .position(position) + } + } + // MARK: - Legends public func setupLegends() { diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index 45ac8387..bed7e346 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -297,7 +297,56 @@ public final class MultiLineChartData: LineChartDataProtocol { } return locations } - + public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { + return ZStack { + ForEach(self.dataSets.dataSets, id: \.self) { dataSet in + let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + dataSet: dataSet, + touchLocation: touchLocation) + + switch self.chartStyle.markerType { + case .vertical: + Vertical(position: position) + .stroke(Color.primary, lineWidth: 2) + case .rectangle: + RoundedRectangle(cornerRadius: 25.0, style: .continuous) + .fill(Color.clear) + .frame(width: 100, height: chartSize.frame(in: .local).height) + .position(x: position.x, + y: chartSize.frame(in: .local).midY) + .overlay( + RoundedRectangle(cornerRadius: 25.0, style: .continuous) + .stroke(Color.primary, lineWidth: 2) + .shadow(color: .primary, radius: 4, x: 0, y: 0) + .frame(width: 50, height: chartSize.frame(in: .local).height) + .position(x: position.x, + y: chartSize.frame(in: .local).midY) + ) + 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) + } + + + PosistionIndicator() + .frame(width: 15, height: 15) + .position(position) + } + } + + } // MARK: - Legends public func setupLegends() { for dataSet in dataSets.dataSets { diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index 936255a2..764e0579 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -25,9 +25,51 @@ extension LineChartDataProtocol { return self.locationOnPath(getPercentageOfPath(path: path, touchLocation: touchLocation), path) } - + /** + Returns the relevent path based on the line type. + + - Parameters: + - style: Styling 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 + */ + func getPath(style: LineStyle, rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, touchLocation: CGPoint, isFilled: Bool) -> Path { + switch style.lineType { + case .line: + return Path.straightLine(rect : rect, + dataPoints : dataPoints, + minValue : minValue, + range : range, + isFilled : isFilled) + case .curvedLine: + return Path.curvedLine(rect : rect, + dataPoints : dataPoints, + minValue : minValue, + range : range, + isFilled : isFilled) + } + } // Maybe put all into extentions of: Path / CGPoint / CGFloat // https://developer.apple.com/documentation/swiftui/path/element + /** + 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. + */ + 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. @@ -120,51 +162,7 @@ extension LineChartDataProtocol { } return total } - /** - Returns the relevent path based on the line type. - - - Parameters: - - style: Styling 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 - */ - func getPath(style: LineStyle, rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, touchLocation: CGPoint, isFilled: Bool) -> Path { - switch style.lineType { - case .line: - return Path.straightLine(rect : rect, - dataPoints : dataPoints, - minValue : minValue, - range : range, - isFilled : isFilled) - case .curvedLine: - return Path.curvedLine(rect : rect, - dataPoints : dataPoints, - minValue : minValue, - range : range, - isFilled : isFilled) - } - } - - /** - 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. - */ - 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 - } - + /** Returns a point on the path based on the location of the touch or pointer input on the X axis. diff --git a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift index 071ca417..d9ddd3e2 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift @@ -38,6 +38,7 @@ public final class DoughnutChartData: DoughnutChartDataProtocol { } public func getXAxisLabels() -> some View { EmptyView() } + public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint diff --git a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift index e63da877..b4000bf4 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift @@ -38,6 +38,7 @@ public final class PieChartData: PieChartDataProtocol { } public func getXAxisLabels() -> some View { EmptyView() } + public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 4169e643..1236bc30 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -21,8 +21,8 @@ public protocol ChartData: ObservableObject, Identifiable { associatedtype DataPoint: CTChartDataPoint associatedtype CTStyle : CTChartStyle - associatedtype Body : View - + associatedtype XLabels : View + associatedtype Touch : View var id: ID { get } /** @@ -148,9 +148,10 @@ public protocol ChartData: ObservableObject, Identifiable { - Tag: getXAxidLabels */ - func getXAxisLabels() -> Body + func getXAxisLabels() -> XLabels + + func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> Touch } - // MARK: - Data Sets /** Main protocol set conformace for types of Data Sets. diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index d637c65a..5cc62afc 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -17,9 +17,7 @@ import SwiftUI internal struct TouchOverlay: ViewModifier where T: ChartData { @ObservedObject var chartData: T - - private var markerType : MarkerType - + /// Current location of the touch input @State private var touchLocation : CGPoint = CGPoint(x: 0, y: 0) /// Frame information of the data point information box @@ -30,11 +28,9 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { /// - chartData: /// - specifier: Decimal precision for labels internal init(chartData : T, - specifier : String, - markerType : MarkerType + specifier : String ) { self.chartData = chartData - self.markerType = markerType self.chartData.infoView.touchSpecifier = specifier } internal func body(content: Content) -> some View { @@ -62,192 +58,15 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { chartData.infoView.touchOverlayInfo = [] } ) - /* - TODO: ------------------------------- - Choose attachement style for markers - Add touch event function to protocol - */ if chartData.infoView.isTouchCurrent { - - switch chartData.chartType { - case (.line, .single): - Text("") - if let data = chartData as? LineChartData { - let position = data.getIndicatorLocation(rect: geo.frame(in: .global), - dataSet: data.dataSets, - touchLocation: touchLocation) - - switch markerType { - case .vertical: - Vertical(position: position) - .stroke(Color.primary, lineWidth: 2) - case .rectangle: - RoundedRectangle(cornerRadius: 25.0, style: .continuous) - .fill(Color.clear) - .frame(width: 100, height: geo.frame(in: .local).height) - .position(x: position.x, - y: geo.frame(in: .local).midY) - .overlay( - RoundedRectangle(cornerRadius: 25.0, style: .continuous) - .stroke(Color.primary, lineWidth: 2) - .shadow(color: .primary, radius: 4, x: 0, y: 0) - .frame(width: 50, height: geo.frame(in: .local).height) - .position(x: position.x, - y: geo.frame(in: .local).midY) - ) - 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) - } - - PosistionIndicator() - .frame(width: 15, height: 15) - .position(position) - - } - - case (.line, .multi): - - if let data = chartData as? MultiLineChartData { - - ForEach(data.dataSets.dataSets, id: \.self) { dataSet in - let position = data.getIndicatorLocation(rect: geo.frame(in: .global), - dataSet: dataSet, - touchLocation: touchLocation) - - switch markerType { - case .vertical: - Vertical(position: position) - .stroke(Color.primary, lineWidth: 2) - case .rectangle: - RoundedRectangle(cornerRadius: 25.0, style: .continuous) - .fill(Color.clear) - .frame(width: 100, height: geo.frame(in: .local).height) - .position(x: position.x, - y: geo.frame(in: .local).midY) - .overlay( - RoundedRectangle(cornerRadius: 25.0, style: .continuous) - .stroke(Color.primary, lineWidth: 2) - .shadow(color: .primary, radius: 4, x: 0, y: 0) - .frame(width: 50, height: geo.frame(in: .local).height) - .position(x: position.x, - y: geo.frame(in: .local).midY) - ) - 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) - } - - - PosistionIndicator() - .frame(width: 15, height: 15) - .position(position) - } - } - - case (.bar, .single): - - if let data = chartData as? BarChartData { - - let positions = data.getPointLocation(touchLocation: touchLocation, - chartSize: geo) - ForEach(positions, id: \.self) { position in - - switch markerType { - case .vertical: - MarkerFull(position: position) - .stroke(Color.primary, lineWidth: 2) - case .rectangle: - 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) - } - } - - } - case (.bar, .multi): - if let data = chartData as? GroupedBarChartData { - - let positions = data.getPointLocation(touchLocation: touchLocation, - chartSize: geo) - ForEach(positions, id: \.self) { position in - - switch markerType { - case .vertical: - MarkerFull(position: position) - .stroke(Color.primary, lineWidth: 2) - case .rectangle: - 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) - } - } - } - - case (.pie, .single): - Text("") - case (.pie, .multi): - Text("") - } + chartData.touchInteraction(touchLocation: touchLocation, chartSize: geo) } } } } else { content } } } - + // MOVE TO PROTOCOL -- SEE INFOBOX /// Sets the point info box location while keeping it within the parent view. /// - Parameters: /// - boxFrame: The size of the point info box. @@ -290,12 +109,10 @@ extension View { - Tag: TouchOverlay */ public func touchOverlay(chartData: T, - specifier: String = "%.0f", - markerType: MarkerType = .vertical + specifier: String = "%.0f" ) -> some View { self.modifier(TouchOverlay(chartData: chartData, - specifier: specifier, - markerType: markerType)) + specifier: specifier)) } #elseif os(tvOS) /** From bfbb354c8118f5c09a7ac9b81a518647aa3bee58 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 18 Feb 2021 16:29:34 +0000 Subject: [PATCH 070/152] Tidy up. --- Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 5cc62afc..101e55c3 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -47,9 +47,7 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { chartData.infoView.isTouchCurrent = true chartData.infoView.touchOverlayInfo = chartData.getDataPoint(touchLocation: touchLocation, chartSize: geo) - chartData.infoView.positionX = setBoxLocationation(touchLocation: touchLocation, boxFrame: boxFrame, chartSize: geo).x - chartData.infoView.frame = geo.frame(in: .local) } From 93abe369b7c9977d7c7c686240b2399cb923eed4 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 18 Feb 2021 16:29:51 +0000 Subject: [PATCH 071/152] Fix scaling of legends. --- .../Shared/Views/LegendView.swift | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index 61b187a6..3e5d8f33 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -36,26 +36,47 @@ internal struct LegendView: View where T: ChartData { case .bar: bar(legend) -// .if(self.scaleLegend(legend: legend)) { $0.scaleEffect(1.2, anchor: .leading) } + .if(scaleLegendBar(legend: legend)) { $0.scaleEffect(1.2, anchor: .leading) } case .pie: pie(legend) - .if(chartData.infoView.isTouchCurrent && legend.id == chartData.infoView.touchOverlayInfo[0].id as! UUID) { $0.scaleEffect(1.2, anchor: .leading) } + .if(scaleLegendPie(legend: legend)) { + $0.scaleEffect(1.2, anchor: .leading) + } } } }.id(UUID()) } - private func scaleLegend(legend: LegendData) -> Bool { - var matched : Bool = false - chartData.infoView.touchOverlayInfo.forEach { (dataPoint) in - if matched { return } - if legend.id == dataPoint.id as! UUID { - matched = true + 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? GroupedBarChartDataPoint { + return chartData.infoView.isTouchCurrent && legend.colour == datapoint.colour + } else { + return false } + } else { + return false } - return matched } - + 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 + } + } func line(_ legend: LegendData) -> some View { Group { From 6411dedeb34e89d357137a25bb778c5f6e675f57 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 18 Feb 2021 17:44:41 +0000 Subject: [PATCH 072/152] Add grouping for Multi part bar charts. --- .../BarChart/Models/BarChartDataPoint.swift | 109 +++++++++--------- .../ChartData/GroupedBarChartData.swift | 65 +++++++---- .../ChartData/StackedBarChartData.swift | 54 ++++++--- .../Models/DataSet/MultiBarDataSets.swift | 6 +- .../Models/Protocols/BarChartProtocols.swift | 15 ++- .../BarChart/Views/GroupedBarChart.swift | 20 ++-- .../BarChart/Views/StackedBarChart.swift | 20 ++-- .../Models/Protocols/LineChartProtocols.swift | 6 + .../Models/Protocols/SharedProtocols.swift | 7 +- .../Shared/Views/LegendView.swift | 2 +- 10 files changed, 179 insertions(+), 125 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift index ca76bb96..2632972a 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift @@ -167,82 +167,83 @@ public struct BarChartDataPoint: CTStandardBarDataPoint { } } - -// MARK: - Grouped -public struct GroupedBarChartDataPoint: CTGroupedBarDataPoint { - - public let id = UUID() - - public var value : Double - public var xAxisLabel : String? - public var pointDescription : String? - public var date : Date? +public struct GroupingData: CTColourStyle, Hashable, Identifiable { - public var colourType : ColourType - public var colour : Color? - public var colours : [Color]? - public var stops : [GradientStop]? - public var startPoint : UnitPoint? - public var endPoint : UnitPoint? - - public init(value : Double, - xAxisLabel : String? = nil, - pointLabel : String? = nil, - date : Date? = nil, - colour : Color? = nil + public let id : UUID = UUID() + public var title : String + public var colourType: ColourType + public var colour : Color? + public var colours : [Color]? + public var stops : [GradientStop]? + public var startPoint: UnitPoint? + public var endPoint : UnitPoint? + + public init(title : String, + colour : Color ) { - self.value = value - self.xAxisLabel = xAxisLabel - self.pointDescription = pointLabel - self.date = date + self.title = title + self.colourType = .colour self.colour = colour self.colours = nil self.stops = nil self.startPoint = nil self.endPoint = nil - self.colourType = .colour } - public init(value : Double, - xAxisLabel : String? = nil, - pointLabel : String? = nil, - date : Date? = nil, - - colours : [Color]? = nil, - startPoint : UnitPoint? = nil, - endPoint : UnitPoint? = nil + public init(title : String, + colours : [Color], + startPoint : UnitPoint, + endPoint : UnitPoint ) { - self.value = value - self.xAxisLabel = xAxisLabel - self.pointDescription = pointLabel - self.date = date - + self.title = title + self.colourType = .gradientColour self.colour = nil - self.stops = nil self.colours = colours + self.stops = nil self.startPoint = startPoint self.endPoint = endPoint - self.colourType = .gradientColour } - public init(value : Double, - xAxisLabel : String? = nil, - pointLabel : String? = nil, - date : Date? = nil, - stops : [GradientStop]? = nil, - startPoint : UnitPoint? = nil, - endPoint : UnitPoint? = nil + public init(title : String, + stops : [GradientStop], + startPoint : UnitPoint, + endPoint : UnitPoint ) { - self.value = value - self.xAxisLabel = xAxisLabel - self.pointDescription = pointLabel - self.date = date + self.title = title + self.colourType = .gradientStops self.colour = nil self.colours = nil self.stops = stops self.startPoint = startPoint self.endPoint = endPoint - self.colourType = .gradientStops } +} + +// MARK: - Grouped +public struct GroupedBarChartDataPoint: CTGroupedBarDataPoint { + public let id = UUID() + + public var value : Double + public var xAxisLabel : String? + public var pointDescription : String? + public var date : Date? + + public var group : GroupingData + + public init(value : Double, + xAxisLabel : String? = nil, + pointLabel : String? = nil, + date : Date? = nil, + group: GroupingData + ) { + self.value = value + self.xAxisLabel = xAxisLabel + self.pointDescription = pointLabel + self.date = date + + self.group = group + + } + public typealias ID = UUID } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 3d546b6c..7624b0b2 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -189,7 +189,9 @@ public final class GroupedBarChartData: BarChartDataProtocol { @Published public var viewData : ChartViewData @Published public var infoView : InfoViewData = InfoViewData() - public var groupLegends : [GroupedBarLegend] + //ADD TO A PROTOCOL + @Published public var groups : [GroupingData] + public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) @@ -200,12 +202,13 @@ public final class GroupedBarChartData: BarChartDataProtocol { /// /// - Parameters: /// - dataSets: Data to draw and style the bars. + /// - groups: Data for how to group 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. /// - 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 : GroupedBarDataSets, - groupLegends: [GroupedBarLegend], + groups : [GroupingData], metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, barStyle : BarStyle = BarStyle(), @@ -213,11 +216,11 @@ public final class GroupedBarChartData: BarChartDataProtocol { noDataText : Text = Text("No Data") ) { self.dataSets = dataSets + self.groups = groups self.metadata = metadata self.xAxisLabels = xAxisLabels self.barStyle = barStyle self.chartStyle = chartStyle - self.groupLegends = groupLegends self.noDataText = noDataText self.legends = [LegendData]() self.viewData = ChartViewData() @@ -374,29 +377,45 @@ public final class GroupedBarChartData: BarChartDataProtocol { // MARK: - Legends public func setupLegends() { - - for legend in self.groupLegends { - self.legends.append(LegendData(id: UUID(), - legend: legend.title, - colour: legend.colour, - strokeStyle: nil, - prioity: 1, - chartType: .bar)) - } + + for group in self.groups { + + if group.colourType == .colour, + let colour = group.colour + { + self.legends.append(LegendData(id : group.id, + legend : group.title, + colour : colour, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if group.colourType == .gradientColour, + let colours = group.colours + { + self.legends.append(LegendData(id : group.id, + legend : group.title, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if group.colourType == .gradientStops, + let stops = group.stops + { + self.legends.append(LegendData(id : group.id, + legend : group.title, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } + } } public typealias Set = GroupedBarDataSets public typealias DataPoint = GroupedBarChartDataPoint public typealias CTStyle = BarChartStyle } - -public struct GroupedBarLegend { - - public let title : String - public let colour: Color - - public init(title: String, colour: Color) { - self.title = title - self.colour = colour - } -} diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index 14523f19..f09504c1 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -21,12 +21,13 @@ public final class StackedBarChartData: BarChartDataProtocol { @Published public var viewData : ChartViewData @Published public var infoView : InfoViewData = InfoViewData() - public var groupLegends : [GroupedBarLegend] + @Published public var groups : [GroupingData] + public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) public init(dataSets : GroupedBarDataSets, - groupLegends: [GroupedBarLegend], + groups : [GroupingData], metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, barStyle : BarStyle = BarStyle(), @@ -34,11 +35,11 @@ public final class StackedBarChartData: BarChartDataProtocol { noDataText : Text = Text("No Data") ) { self.dataSets = dataSets + self.groups = groups self.metadata = metadata self.xAxisLabels = xAxisLabels self.barStyle = barStyle self.chartStyle = chartStyle - self.groupLegends = groupLegends self.noDataText = noDataText self.legends = [LegendData]() self.viewData = ChartViewData() @@ -51,10 +52,10 @@ public final class StackedBarChartData: BarChartDataProtocol { switch self.chartStyle.xAxisLabelsFrom { case .dataPoint: HStack(spacing: 0) { - ForEach(dataSets.dataSets) { dataSet in + ForEach(groups) { group in Spacer() .frame(minWidth: 0, maxWidth: 500) - Text(dataSet.legendTitle) + Text(group.title) .font(.caption) .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) @@ -221,14 +222,41 @@ public final class StackedBarChartData: BarChartDataProtocol { // MARK: - Legends public func setupLegends() { - for legend in self.groupLegends { - self.legends.append(LegendData(id: UUID(), - legend: legend.title, - colour: legend.colour, - strokeStyle: nil, - prioity: 1, - chartType: .bar)) - } + for group in self.groups { + + if group.colourType == .colour, + let colour = group.colour + { + self.legends.append(LegendData(id : group.id, + legend : group.title, + colour : colour, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if group.colourType == .gradientColour, + let colours = group.colours + { + self.legends.append(LegendData(id : group.id, + legend : group.title, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if group.colourType == .gradientStops, + let stops = group.stops + { + self.legends.append(LegendData(id : group.id, + legend : group.title, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } + } } public typealias Set = GroupedBarDataSets public typealias DataPoint = GroupedBarChartDataPoint diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift index eabf0e90..84e62bfd 100644 --- a/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift +++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift @@ -24,15 +24,11 @@ public struct GroupedBarDataSet: CTGroupedBarChartDataSet { public let id : UUID public var dataPoints : [GroupedBarChartDataPoint] - public var legendTitle : String /// Initialises a new data set for a Bar Chart. - public init(dataPoints : [GroupedBarChartDataPoint], - legendTitle : String - ) { + public init(dataPoints : [GroupedBarChartDataPoint]) { self.id = UUID() self.dataPoints = dataPoints - self.legendTitle = legendTitle } public typealias ID = UUID diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index f348838b..23d22e1e 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -52,9 +52,14 @@ public protocol CTBarChartStyle: CTLineAndBarChartStyle {} - Tag: CTBarChartDataSet */ -public protocol CTStandardBarChartDataSet: SingleDataSet {} +public protocol CTStandardBarChartDataSet: SingleDataSet { + /** + Label to display in the legend. + */ + var legendTitle : String { get set } +} -public protocol CTGroupedBarChartDataSet: SingleDataSet {} +public protocol CTGroupedBarChartDataSet: SingleDataSet {} public protocol CTSStackedBarChartDataSet: SingleDataSet {} @@ -85,4 +90,8 @@ public protocol CTStandardBarDataPoint: CTBarDataPoint, CTColourStyle {} - Tag: CTMultiPartBarDataPoint */ -public protocol CTGroupedBarDataPoint: CTBarDataPoint, CTColourStyle {} +public protocol CTGroupedBarDataPoint: CTBarDataPoint { + + var group : GroupingData { get set } + +} diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index 19833403..e08d0bee 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -28,24 +28,24 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD HStack(spacing: 0) { ForEach(dataSet.dataPoints) { dataPoint in - if dataPoint.colourType == .colour, - let colour = dataPoint.colour + if dataPoint.group.colourType == .colour, + let colour = dataPoint.group.colour { ColourBar(colour, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) - } else if dataPoint.colourType == .gradientColour, - let colours = dataPoint.colours, - let startPoint = dataPoint.startPoint, - let endPoint = dataPoint.endPoint + } else if dataPoint.group.colourType == .gradientColour, + let colours = dataPoint.group.colours, + let startPoint = dataPoint.group.startPoint, + let endPoint = dataPoint.group.endPoint { GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) - } else if dataPoint.colourType == .gradientStops, - let stops = dataPoint.stops, - let startPoint = dataPoint.startPoint, - let endPoint = dataPoint.endPoint + } else if dataPoint.group.colourType == .gradientStops, + let stops = dataPoint.group.stops, + let startPoint = dataPoint.group.startPoint, + let endPoint = dataPoint.group.endPoint { let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) diff --git a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift index 73a4fcfb..99fc8383 100644 --- a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift @@ -57,28 +57,28 @@ internal struct MultiPartBarSubView: View { VStack(spacing: 0) { ForEach(dataSet.dataPoints.reversed()) { dataPoint in - if dataPoint.colourType == .colour, - let colour = dataPoint.colour + if dataPoint.group.colourType == .colour, + let colour = dataPoint.group.colour { ColourPartBar(colour, getHeight(height : geo.size.height, dataSet : dataSet, dataPoint : dataPoint)) - } else if dataPoint.colourType == .gradientColour, - let colours = dataPoint.colours, - let startPoint = dataPoint.startPoint, - let endPoint = dataPoint.endPoint + } else if dataPoint.group.colourType == .gradientColour, + let colours = dataPoint.group.colours, + let startPoint = dataPoint.group.startPoint, + let endPoint = dataPoint.group.endPoint { GradientColoursPartBar(colours, startPoint, endPoint, getHeight(height: geo.size.height, dataSet : dataSet, dataPoint : dataPoint)) - } else if dataPoint.colourType == .gradientStops, - let stops = dataPoint.stops, - let startPoint = dataPoint.startPoint, - let endPoint = dataPoint.endPoint + } else if dataPoint.group.colourType == .gradientStops, + let stops = dataPoint.group.stops, + let startPoint = dataPoint.group.startPoint, + let endPoint = dataPoint.group.endPoint { let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index 7e461470..181ac68f 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -74,6 +74,12 @@ public protocol CTLineChartStyle : CTLineAndBarChartStyle { */ public protocol CTLineChartDataSet: SingleDataSet { associatedtype Styling : CTColourStyle + + /** + Label to display in the legend. + */ + var legendTitle : String { get set } + /** Sets the style for the Data Set (as opposed to Chart Data Style). */ diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 1236bc30..f0a1b612 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -176,12 +176,7 @@ public protocol SingleDataSet: DataSet { [See CTChartDataPoint](x-source-tag://CTChartDataPoint) */ var dataPoints : [DataPoint] { get set } - - /** - Label to display in the legend. - */ - var legendTitle : String { get set } - + } /** diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index 3e5d8f33..0da672b1 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -57,7 +57,7 @@ internal struct LegendView: View where T: ChartData { } } else if chartData is GroupedBarChartData || chartData is StackedBarChartData { if let datapoint = chartData.infoView.touchOverlayInfo.first as? GroupedBarChartDataPoint { - return chartData.infoView.isTouchCurrent && legend.colour == datapoint.colour + return chartData.infoView.isTouchCurrent && legend.colour == datapoint.group.colour } else { return false } From 364efd24c49df7c8580c75e1866d2b594fa0d0b0 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 19 Feb 2021 14:01:28 +0000 Subject: [PATCH 073/152] Refactor Touch. --- .../Models/ChartData/BarChartData.swift | 14 +- .../ChartData/GroupedBarChartData.swift | 15 +- .../ChartData/StackedBarChartData.swift | 14 +- .../{ => Datapoints}/BarChartDataPoint.swift | 81 -------- .../Datapoints/GroupedBarChartDataPoint.swift | 88 +++++++++ .../Models/Protocols/BarChartProtocols.swift | 32 +++- .../BarChartProtocolsExtensions.swift | 20 ++ .../BarChart/Models/Style/BarChartStyle.swift | 2 +- .../Models/ChartData/LineChartData.swift | 57 +----- .../Models/ChartData/MultiLineChartData.swift | 62 +----- .../Models/Protocols/LineChartProtocols.swift | 10 +- .../LineChartProtocolsExtensions.swift | 179 +++++++++++++++++- .../Models/Style/LineChartStyle.swift | 2 +- .../Models/Doughnut/DoughnutChartData.swift | 1 - .../PieChart/Models/Pie/PieChartData.swift | 1 - .../PieChart/Models/PieChartProtocols.swift | 5 - .../Shared/Extras/SharedEnums.swift | 37 +++- .../Shared/Models/InfoViewData.swift | 6 +- .../Models/Protocols/SharedProtocols.swift | 38 ++-- .../Shared/ViewModifiers/HeaderBox.swift | 3 +- .../Shared/ViewModifiers/InfoBox.swift | 2 +- .../Shared/ViewModifiers/TouchOverlay.swift | 16 +- .../Shared/Views/PosistionIndicator.swift | 33 ++++ .../Protocols/LineAndBarProtocols.swift | 14 ++ .../LineAndBarProtocolsExtentions.swift | 5 - 25 files changed, 432 insertions(+), 305 deletions(-) rename Sources/SwiftUICharts/BarChart/Models/{ => Datapoints}/BarChartDataPoint.swift (69%) create mode 100644 Sources/SwiftUICharts/BarChart/Models/Datapoints/GroupedBarChartDataPoint.swift create mode 100644 Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift create mode 100644 Sources/SwiftUICharts/Shared/Views/PosistionIndicator.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 304708c6..45657568 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -1,5 +1,5 @@ // -// File.swift +// BarChartData.swift // // // Created by Will Dale on 23/01/2021. @@ -251,14 +251,6 @@ public final class BarChartData: BarChartDataProtocol { } } } - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let maxValue: Double = self.getMaxValue() - for index in 0...self.chartStyle.yAxisNumberOfLabels { - labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) - } - return labels - } // MARK: - Touch public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { @@ -289,7 +281,9 @@ public final class BarChartData: BarChartDataProtocol { return ZStack { ForEach(positions, id: \.self) { position in - switch self.chartStyle.markerType { + switch self.chartStyle.markerType { + case .none: + EmptyView() case .vertical: MarkerFull(position: position) .stroke(Color.primary, lineWidth: 2) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 7624b0b2..db32b998 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -175,7 +175,7 @@ import SwiftUI - Tag: GroupedBarChartData */ -public final class GroupedBarChartData: BarChartDataProtocol { +public final class GroupedBarChartData: GroupedBarChartDataProtocol { // MARK: - Properties public let id : UUID = UUID() @@ -188,8 +188,6 @@ public final class GroupedBarChartData: BarChartDataProtocol { @Published public var legends : [LegendData] @Published public var viewData : ChartViewData @Published public var infoView : InfoViewData = InfoViewData() - - //ADD TO A PROTOCOL @Published public var groups : [GroupingData] public var noDataText : Text @@ -272,15 +270,6 @@ public final class GroupedBarChartData: BarChartDataProtocol { } } - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let maxValue: Double = self.getMaxValue() - for index in 0...self.chartStyle.yAxisNumberOfLabels { - labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) - } - return labels - } - // MARK: - Touch public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [GroupedBarChartDataPoint] { @@ -349,6 +338,8 @@ public final class GroupedBarChartData: BarChartDataProtocol { ForEach(positions, id: \.self) { position in switch self.chartStyle.markerType { + case .none: + EmptyView() case .vertical: MarkerFull(position: position) .stroke(Color.primary, lineWidth: 2) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index f09504c1..91c8cc60 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -7,7 +7,7 @@ import SwiftUI -public final class StackedBarChartData: BarChartDataProtocol { +public final class StackedBarChartData: GroupedBarChartDataProtocol { // MARK: - Properties public let id : UUID = UUID() @@ -20,7 +20,6 @@ public final class StackedBarChartData: BarChartDataProtocol { @Published public var legends : [LegendData] @Published public var viewData : ChartViewData @Published public var infoView : InfoViewData = InfoViewData() - @Published public var groups : [GroupingData] public var noDataText : Text @@ -83,15 +82,6 @@ public final class StackedBarChartData: BarChartDataProtocol { } } - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let maxValue: Double = self.getMaxValue() - for index in 0...self.chartStyle.yAxisNumberOfLabels { - labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) - } - return labels - } - // MARK: - Touch public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [GroupedBarChartDataPoint] { @@ -194,6 +184,8 @@ public final class StackedBarChartData: BarChartDataProtocol { ForEach(positions, id: \.self) { position in switch self.chartStyle.markerType { + case .none: + EmptyView() case .vertical: MarkerFull(position: position) .stroke(Color.primary, lineWidth: 2) diff --git a/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift similarity index 69% rename from Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift rename to Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift index 2632972a..141f8bf2 100644 --- a/Sources/SwiftUICharts/BarChart/Models/BarChartDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift @@ -166,84 +166,3 @@ public struct BarChartDataPoint: CTStandardBarDataPoint { self.colourType = .gradientStops } } - -public struct GroupingData: CTColourStyle, Hashable, Identifiable { - - public let id : UUID = UUID() - public var title : String - public var colourType: ColourType - public var colour : Color? - public var colours : [Color]? - public var stops : [GradientStop]? - public var startPoint: UnitPoint? - public var endPoint : UnitPoint? - - public init(title : String, - colour : Color - ) { - self.title = title - self.colourType = .colour - self.colour = colour - self.colours = nil - self.stops = nil - self.startPoint = nil - self.endPoint = nil - } - - public init(title : String, - colours : [Color], - startPoint : UnitPoint, - endPoint : UnitPoint - ) { - self.title = title - self.colourType = .gradientColour - self.colour = nil - self.colours = colours - self.stops = nil - self.startPoint = startPoint - self.endPoint = endPoint - } - - public init(title : String, - stops : [GradientStop], - startPoint : UnitPoint, - endPoint : UnitPoint - ) { - self.title = title - self.colourType = .gradientStops - self.colour = nil - self.colours = nil - self.stops = stops - self.startPoint = startPoint - self.endPoint = endPoint - } -} - -// MARK: - Grouped -public struct GroupedBarChartDataPoint: CTGroupedBarDataPoint { - - public let id = UUID() - - public var value : Double - public var xAxisLabel : String? - public var pointDescription : String? - public var date : Date? - - public var group : GroupingData - - public init(value : Double, - xAxisLabel : String? = nil, - pointLabel : String? = nil, - date : Date? = nil, - group: GroupingData - ) { - self.value = value - self.xAxisLabel = xAxisLabel - self.pointDescription = pointLabel - self.date = date - - self.group = group - - } - public typealias ID = UUID -} diff --git a/Sources/SwiftUICharts/BarChart/Models/Datapoints/GroupedBarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/GroupedBarChartDataPoint.swift new file mode 100644 index 00000000..9221d5d1 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/GroupedBarChartDataPoint.swift @@ -0,0 +1,88 @@ +// +// GroupedBarChartDataPoint.swift +// +// +// Created by Will Dale on 19/02/2021. +// + +import SwiftUI + +public struct GroupedBarChartDataPoint: CTGroupedBarDataPoint { + + public let id = UUID() + + public var value : Double + public var xAxisLabel : String? + public var pointDescription : String? + public var date : Date? + + public var group : GroupingData + + public init(value : Double, + xAxisLabel : String? = nil, + pointLabel : String? = nil, + date : Date? = nil, + group: GroupingData + ) { + self.value = value + self.xAxisLabel = xAxisLabel + self.pointDescription = pointLabel + self.date = date + + self.group = group + + } + public typealias ID = UUID +} + +public struct GroupingData: CTColourStyle, Hashable, Identifiable { + + public let id : UUID = UUID() + public var title : String + public var colourType: ColourType + public var colour : Color? + public var colours : [Color]? + public var stops : [GradientStop]? + public var startPoint: UnitPoint? + public var endPoint : UnitPoint? + + public init(title : String, + colour : Color + ) { + self.title = title + self.colourType = .colour + self.colour = colour + self.colours = nil + self.stops = nil + self.startPoint = nil + self.endPoint = nil + } + + public init(title : String, + colours : [Color], + startPoint : UnitPoint, + endPoint : UnitPoint + ) { + self.title = title + self.colourType = .gradientColour + self.colour = nil + self.colours = colours + self.stops = nil + self.startPoint = startPoint + self.endPoint = endPoint + } + + public init(title : String, + stops : [GradientStop], + startPoint : UnitPoint, + endPoint : UnitPoint + ) { + self.title = title + self.colourType = .gradientStops + self.colour = nil + self.colours = nil + self.stops = stops + self.startPoint = startPoint + self.endPoint = endPoint + } +} diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index 23d22e1e..c8337b0f 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -13,7 +13,9 @@ import SwiftUI A protocol to extend functionality of `LineAndBarChartData` specifically for Bar Charts. # Reference - [See LineAndBarChartData](x-source-tag://LineAndBarChartData) + - [See LineAndBarChartData](x-source-tag://LineAndBarChartData) + - [See LineAndBarChartData](x-source-tag://LineAndBarChartData) + - [See ChartData](x-source-tag://ChartData) `LineAndBarChartData` conforms to [ChartData](x-source-tag://ChartData) @@ -21,7 +23,33 @@ import SwiftUI */ public protocol BarChartDataProtocol: LineAndBarChartData { - var barStyle : BarStyle { get set } + /** + Overall styling for the bars + */ + var barStyle : BarStyle { get set } +} + +/** + A protocol to extend functionality of `LineAndBarChartData` specifically for Bar Charts. + + # Reference + - [See GroupedBarChartDataProtocol](x-source-tag://GroupedBarChartDataProtocol) + - [See BarChartDataProtocol](x-source-tag://BarChartDataProtocol) + - [See LineAndBarChartData](x-source-tag://LineAndBarChartData) + - [See ChartData](x-source-tag://ChartData) + + `GroupedBarChartDataProtocol` conforms to [BarChartDataProtocol](x-source-tag://ChartData) + + `LineAndBarChartData` conforms to [ChartData](x-source-tag://ChartData) + + - Tag: GroupedBarChartDataProtocol + */ +public protocol GroupedBarChartDataProtocol: BarChartDataProtocol { + + /** + Grouping data to inform the chart about the relationship between the datapoints. + */ + var groups : [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..e104d612 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift @@ -0,0 +1,20 @@ +// +// BarChartProtocolsExtensions.swift +// +// +// Created by Will Dale on 19/02/2021. +// + +import SwiftUI + +// Standard / Grouped / Stacked +extension LineAndBarChartData where Self: BarChartDataProtocol { + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let maxValue: Double = self.getMaxValue() + for index in 0...self.chartStyle.yAxisNumberOfLabels { + labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) + } + return labels + } +} diff --git a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift index b7310dc1..78f23319 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift @@ -103,7 +103,7 @@ public struct BarChartStyle: CTBarChartStyle { public init(infoBoxPlacement : InfoBoxPlacement = .floating, infoBoxValueColour : Color = Color.primary, infoBoxDescriptionColor : Color = Color.primary, - markerType : MarkerType = .full, + markerType : MarkerType = .full(attachment: .line), xAxisGridStyle : GridStyle = GridStyle(), xAxisLabelPosition : XAxisLabelPosistion = .bottom, diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index 7c0fae40..6f80f483 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -241,19 +241,6 @@ public final class LineChartData: LineChartDataProtocol { } } - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let dataRange : Double = self.getRange() - let minValue : Double = self.getMinValue() - let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) - - labels.append(minValue) - for index in 1...self.chartStyle.yAxisNumberOfLabels { - labels.append(minValue + range * Double(index)) - } - return labels - } - // MARK: - Touch public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [LineChartDataPoint] { var points : [LineChartDataPoint] = [] @@ -283,49 +270,7 @@ public final class LineChartData: LineChartDataProtocol { } public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { - let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), - dataSet: dataSets, - touchLocation: touchLocation) - return ZStack { - switch self.chartStyle.markerType { - case .vertical: - Vertical(position: position) - .stroke(Color.primary, lineWidth: 2) - case .rectangle: - RoundedRectangle(cornerRadius: 25.0, style: .continuous) - .fill(Color.clear) - .frame(width: 100, height: chartSize.frame(in: .local).height) - .position(x: position.x, - y: chartSize.frame(in: .local).midY) - .overlay( - RoundedRectangle(cornerRadius: 25.0, style: .continuous) - .stroke(Color.primary, lineWidth: 2) - .shadow(color: .primary, radius: 4, x: 0, y: 0) - .frame(width: 50, height: chartSize.frame(in: .local).height) - .position(x: position.x, - y: chartSize.frame(in: .local).midY) - ) - 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) - } - - PosistionIndicator() - .frame(width: 15, height: 15) - .position(position) - } + self.markerSubView(dataSet: self.dataSets, touchLocation: touchLocation, chartSize: chartSize) } // MARK: - Legends diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index bed7e346..308ad2f2 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -254,19 +254,6 @@ public final class MultiLineChartData: LineChartDataProtocol { } } - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let dataRange : Double = self.getRange() - let minValue : Double = self.getMinValue() - let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) - - labels.append(minValue) - for index in 1...self.chartStyle.yAxisNumberOfLabels { - labels.append(minValue + range * Double(index)) - } - return labels - } - // MARK: - Touch public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [LineChartDataPoint] { var points : [LineChartDataPoint] = [] @@ -297,56 +284,15 @@ public final class MultiLineChartData: LineChartDataProtocol { } return locations } + public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { - return ZStack { + ZStack { ForEach(self.dataSets.dataSets, id: \.self) { dataSet in - let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), - dataSet: dataSet, - touchLocation: touchLocation) - - switch self.chartStyle.markerType { - case .vertical: - Vertical(position: position) - .stroke(Color.primary, lineWidth: 2) - case .rectangle: - RoundedRectangle(cornerRadius: 25.0, style: .continuous) - .fill(Color.clear) - .frame(width: 100, height: chartSize.frame(in: .local).height) - .position(x: position.x, - y: chartSize.frame(in: .local).midY) - .overlay( - RoundedRectangle(cornerRadius: 25.0, style: .continuous) - .stroke(Color.primary, lineWidth: 2) - .shadow(color: .primary, radius: 4, x: 0, y: 0) - .frame(width: 50, height: chartSize.frame(in: .local).height) - .position(x: position.x, - y: chartSize.frame(in: .local).midY) - ) - 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) + self.markerSubView(dataSet: dataSet, touchLocation: touchLocation, chartSize: chartSize) } - - - PosistionIndicator() - .frame(width: 15, height: 15) - .position(position) } - } - } + // MARK: - Legends public func setupLegends() { for dataSet in dataSets.dataSets { diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index 181ac68f..2bef700b 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -20,6 +20,7 @@ import SwiftUI */ public protocol LineChartDataProtocol: LineAndBarChartData { + associatedtype Bob : View /** Whether it is a normal or filled line. */ @@ -35,7 +36,14 @@ public protocol LineChartDataProtocol: LineAndBarChartData { - touchLocation: Location of the touch or pointer input. - Returns: The position to place the indicator. */ - func getIndicatorLocation(rect: CGRect, dataSet: LineDataSet, touchLocation: CGPoint) -> CGPoint + func getIndicatorLocation(rect: CGRect, dataPoints: [LineChartDataPoint], touchLocation: CGPoint, lineType: LineType) -> CGPoint + + func getSinglePoint(touchLocation: CGPoint, chartSize: GeometryProxy, dataSet: LineDataSet) -> CGPoint + + func markerSubView(dataSet : LineDataSet, + touchLocation : CGPoint, + chartSize : GeometryProxy + ) -> Bob } diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index 764e0579..fbfdb560 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -7,17 +7,33 @@ import SwiftUI +// Standard / Multi +extension LineAndBarChartData where Self: LineChartDataProtocol { + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let dataRange : Double = self.getRange() + let minValue : Double = self.getMinValue() + let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) + + labels.append(minValue) + for index in 1...self.chartStyle.yAxisNumberOfLabels { + labels.append(minValue + range * Double(index)) + } + return labels + } +} + // MARK: - Position Indicator extension LineChartDataProtocol { - public func getIndicatorLocation(rect: CGRect, - dataSet: LineDataSet, - touchLocation: CGPoint + dataPoints: [LineChartDataPoint], + touchLocation: CGPoint, + lineType: LineType ) -> CGPoint { - let path = getPath(style : dataSet.style, + let path = getPath(lineType : lineType, rect : rect, - dataPoints : dataSet.dataPoints, + dataPoints : dataPoints, minValue : self.getMinValue(), range : self.getRange(), touchLocation: touchLocation, @@ -38,8 +54,8 @@ extension LineChartDataProtocol { - isFilled: Whether it is a normal or filled line. - Returns: The relevent path based on the line type */ - func getPath(style: LineStyle, rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, touchLocation: CGPoint, isFilled: Bool) -> Path { - switch style.lineType { + func getPath(lineType: LineType, rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, touchLocation: CGPoint, isFilled: Bool) -> Path { + switch lineType { case .line: return Path.straightLine(rect : rect, dataPoints : dataPoints, @@ -229,3 +245,152 @@ extension LineChartDataProtocol { y: trimmedPoint.boundingRect.midY) } } + +// MARK: - Markers +extension LineChartDataProtocol { + + public func getSinglePoint(touchLocation: CGPoint, chartSize: GeometryProxy, dataSet: LineDataSet) -> CGPoint { + let minValue : Double = self.getMinValue() + let range : Double = self.getRange() + + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) + let ySection : CGFloat = chartSize.size.height / CGFloat(range) + let index : Int = Int((touchLocation.x + (xSection / 2)) / xSection) + if index >= 0 && index < dataSet.dataPoints.count { + return CGPoint(x: CGFloat(index) * xSection, + y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.size.height) + } + return .zero + } + + @ViewBuilder public func markerSubView(dataSet : LineDataSet, + touchLocation : CGPoint, + chartSize : GeometryProxy + ) -> some View { + + switch self.chartStyle.markerType { + case .none: + EmptyView() + case .rectangle: + + let position : CGPoint = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + dataPoints: dataSet.dataPoints, + touchLocation: touchLocation, + lineType: dataSet.style.lineType) + RoundedRectangle(cornerRadius: 25.0, style: .continuous) + .fill(Color.clear) + .frame(width: 100, height: chartSize.frame(in: .local).height) + .position(x: position.x, + y: chartSize.frame(in: .local).midY) + .overlay( + RoundedRectangle(cornerRadius: 25.0, style: .continuous) + .stroke(Color.primary, lineWidth: 2) + .shadow(color: .primary, radius: 4, x: 0, y: 0) + .frame(width: 50, height: chartSize.frame(in: .local).height) + .position(x: position.x, + y: chartSize.frame(in: .local).midY) + ) + + PosistionIndicator() + .frame(width: 15, height: 15) + .position(position) + + case .vertical(attachment: let attach): + + switch attach { + case .line: + Vertical(position: self.getIndicatorLocation(rect: chartSize.frame(in: .global), + dataPoints: dataSet.dataPoints, + touchLocation: touchLocation, + lineType: dataSet.style.lineType)) + .stroke(Color.primary, lineWidth: 2) + case .point: + Vertical(position: self.getSinglePoint(touchLocation: touchLocation, + chartSize: chartSize, + dataSet: dataSet)) + .stroke(Color.primary, lineWidth: 2) + } + + case .full(attachment: let attach): + + switch attach { + case .line: + MarkerFull(position: self.getIndicatorLocation(rect: chartSize.frame(in: .global), + dataPoints: dataSet.dataPoints, + touchLocation: touchLocation, + lineType: dataSet.style.lineType)) + .stroke(Color.primary, lineWidth: 2) + case .point: + MarkerFull(position: self.getSinglePoint(touchLocation: touchLocation, + chartSize: chartSize, + dataSet: dataSet)) + .stroke(Color.primary, lineWidth: 2) + } + + case .bottomLeading(attachment: let attach): + + switch attach { + case .line: + MarkerBottomLeading(position: self.getIndicatorLocation(rect: chartSize.frame(in: .global), + dataPoints: dataSet.dataPoints, + touchLocation: touchLocation, + lineType: dataSet.style.lineType)) + .stroke(Color.primary, lineWidth: 2) + case .point: + MarkerBottomLeading(position: self.getSinglePoint(touchLocation: touchLocation, + chartSize: chartSize, + dataSet: dataSet)) + .stroke(Color.primary, lineWidth: 2) + } + + case .bottomTrailing(attachment: let attach): + + switch attach { + case .line: + MarkerBottomTrailing(position: self.getIndicatorLocation(rect: chartSize.frame(in: .global), + dataPoints: dataSet.dataPoints, + touchLocation: touchLocation, + lineType: dataSet.style.lineType)) + .stroke(Color.primary, lineWidth: 2) + case .point: + MarkerBottomTrailing(position: self.getSinglePoint(touchLocation: touchLocation, + chartSize: chartSize, + dataSet: dataSet)) + .stroke(Color.primary, lineWidth: 2) + } + + case .topLeading(attachment: let attach): + + switch attach { + case .line: + MarkerTopLeading(position: self.getIndicatorLocation(rect: chartSize.frame(in: .global), + dataPoints: dataSet.dataPoints, + touchLocation: touchLocation, + lineType: dataSet.style.lineType)) + .stroke(Color.primary, lineWidth: 2) + case .point: + MarkerTopLeading(position: self.getSinglePoint(touchLocation: touchLocation, + chartSize: chartSize, + dataSet: dataSet)) + .stroke(Color.primary, lineWidth: 2) + } + + case .topTrailing(attachment: let attach): + + switch attach { + case .line: + MarkerTopTrailing(position: self.getIndicatorLocation(rect: chartSize.frame(in: .global), + dataPoints: dataSet.dataPoints, + touchLocation: touchLocation, + lineType: dataSet.style.lineType)) + .stroke(Color.primary, lineWidth: 2) + case .point: + MarkerTopTrailing(position: self.getSinglePoint(touchLocation: touchLocation, + chartSize: chartSize, + dataSet: dataSet)) + .stroke(Color.primary, lineWidth: 2) + } + } + + } +} diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift index cd689a98..e0fe941c 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift @@ -113,7 +113,7 @@ public struct LineChartStyle: CTLineChartStyle { infoBoxValueColour : Color = Color.primary, infoBoxDescriptionColor : Color = Color.primary, - markerType : MarkerType = .vertical, + markerType : MarkerType = .rectangle, markerAttachemnt : MarkerAttachemnt = .line, xAxisGridStyle : GridStyle = GridStyle(), diff --git a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift index d9ddd3e2..bcbc391e 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift @@ -37,7 +37,6 @@ public final class DoughnutChartData: DoughnutChartDataProtocol { self.makeDataPoints() } - public func getXAxisLabels() -> some View { EmptyView() } public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } public typealias Set = PieDataSet diff --git a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift index b4000bf4..294392ce 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift @@ -37,7 +37,6 @@ public final class PieChartData: PieChartDataProtocol { self.makeDataPoints() } - public func getXAxisLabels() -> some View { EmptyView() } public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } public typealias Set = PieDataSet diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift index 19b94e4f..dff600ba 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift @@ -45,11 +45,6 @@ public protocol CTMultiPieChartDataPoints: CTChartDataPoint {} public protocol CTMultiPieDataSet: DataSet {} // MARK: - Pie and Doughnut -extension PieAndDoughnutChartDataProtocol { - public func getHeaderLocation() -> InfoBoxPlacement { - return self.chartStyle.infoBoxPlacement - } -} extension PieAndDoughnutChartDataProtocol where Set == PieDataSet { internal func makeDataPoints() { diff --git a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift index 21b7cf07..139de316 100644 --- a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift @@ -65,18 +65,19 @@ public enum ColourType { /** Placement of the data point information panel when touch overlay modifier is applied. ``` - case floating // Follows input across the chart - case fixed - case header // Fix in the Header box. Must have .headerBox() + case floating // Follows input across the chart. + case fixed // Centered in view. + case header // Fix in the Header box. Must have .headerBox(). ``` - Tag: InfoBoxPlacement */ public enum InfoBoxPlacement { - /// Follows input across the chart + /// Follows input across the chart. case floating + /// Centered in view. case fixed - /// Fix in the Header box. Must have .headerBox() + /// Fix in the Header box. Must have .headerBox(). case header } @@ -84,8 +85,9 @@ public enum InfoBoxPlacement { /** Where the marker lines come from to meet at a specified point. ``` - case vertical // Vertical line from top to bottom. + case none // No overlay markers. case rectangle // Rounded rectangle. + case vertical // Vertical line from top to bottom. case full // 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. @@ -97,10 +99,29 @@ public enum InfoBoxPlacement { - Tag: MarkerType */ public enum MarkerType { - /// Vertical line from top to bottom. - case vertical + /// No overlay markers. + case none /// Rounded rectangle. case rectangle + /// 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) +} + +public enum BarMarkerType { + /// 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. diff --git a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift index 31b29e14..42299810 100644 --- a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift @@ -42,7 +42,7 @@ public struct InfoViewData { */ var touchSpecifier : String = "%.0f" - var positionX : CGFloat = 0 - var frame : CGRect = .zero - var yAxisLabelWidth: CGFloat = 0 + var positionX : CGFloat = 0 + var frame : CGRect = .zero + var yAxisLabelWidth : CGFloat = 0 } diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index f0a1b612..9d173561 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -17,12 +17,12 @@ import SwiftUI - Tag: ChartData */ public protocol ChartData: ObservableObject, Identifiable { + associatedtype Set : DataSet associatedtype DataPoint: CTChartDataPoint associatedtype CTStyle : CTChartStyle + associatedtype Touch : View - associatedtype XLabels : View - associatedtype Touch : View var id: ID { get } /** @@ -91,18 +91,6 @@ public protocol ChartData: ObservableObject, Identifiable { */ func legendOrder() -> [LegendData] - /** - Gets the where to display the touch overlay information. - - Returns: Where to display the data points - - # Reference - [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) - - - Tag: getHeaderLocation - */ - func getHeaderLocation() -> InfoBoxPlacement - - /** Configures the legends based on the type of chart. @@ -119,7 +107,7 @@ public protocol ChartData: ObservableObject, Identifiable { /** Gets the nearest data points to the touch location. - Parameters: - - touchLocation: Current location of the touch + - touchLocation: Current location of the touch. - chartSize: The size of the chart view as the parent view. - Returns: Array of data points. @@ -130,28 +118,26 @@ public protocol ChartData: ObservableObject, Identifiable { /** Gets the location of the data point in the view. - Parameters: - - touchLocation: Current location of the touch + - 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 + - Returns: Array of points with the location on screen of data points. - Tag: getDataPoint */ func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] + /** - Displays a view for the labels on the X Axis. - - Labels can come from either [CTChartDataPoint](x-source-tag://CTChartDataPoint) - or [ChartData](x-source-tag://ChartData) + Takes touch location and return a view based on the chart type and configuration. - - Returns: An `HStack` of `Text` containin x axis labels. - - - Tag: getXAxidLabels + - 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 getXAxisLabels() -> XLabels - func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> Touch } + // MARK: - Data Sets /** Main protocol set conformace for types of Data Sets. diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index d6b906bf..b7267f3b 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -52,8 +52,7 @@ internal struct HeaderBox: ViewModifier where T: ChartData { Group { #if !os(tvOS) if chartData.isGreaterThanTwo() { - - switch chartData.getHeaderLocation() { + switch chartData.chartStyle.infoBoxPlacement { case .floating: VStack(alignment: .leading) { titleBox diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift index 9d51f608..7e960b2b 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift @@ -15,7 +15,7 @@ internal struct InfoBox: ViewModifier where T: ChartData { internal func body(content: Content) -> some View { VStack { - switch chartData.getHeaderLocation() { + switch chartData.chartStyle.infoBoxPlacement { case .floating: floating case .fixed: diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 101e55c3..d241fd2b 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -36,7 +36,6 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { internal func body(content: Content) -> some View { Group { if chartData.isGreaterThanTwo() { - GeometryReader { geo in ZStack { content @@ -47,8 +46,8 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { chartData.infoView.isTouchCurrent = true chartData.infoView.touchOverlayInfo = chartData.getDataPoint(touchLocation: touchLocation, chartSize: geo) - chartData.infoView.positionX = setBoxLocationation(touchLocation: touchLocation, boxFrame: boxFrame, chartSize: geo).x - chartData.infoView.frame = geo.frame(in: .local) + chartData.infoView.positionX = setBoxLocationation(touchLocation: touchLocation, boxFrame: boxFrame, chartSize: geo).x + chartData.infoView.frame = geo.frame(in: .local) } .onEnded { _ in @@ -120,18 +119,9 @@ extension View { Unavailable in tvOS */ public func touchOverlay(chartData: T, - specifier: String = "%.0f", - markerType: MarkerType = .fullWidth + specifier: String = "%.0f" ) -> some View { self.modifier(EmptyModifier()) } #endif } - -struct PosistionIndicator: View { - - var body: some View { - Circle() - .strokeBorder(Color.red, lineWidth: 3) - } -} diff --git a/Sources/SwiftUICharts/Shared/Views/PosistionIndicator.swift b/Sources/SwiftUICharts/Shared/Views/PosistionIndicator.swift new file mode 100644 index 00000000..c319fe41 --- /dev/null +++ b/Sources/SwiftUICharts/Shared/Views/PosistionIndicator.swift @@ -0,0 +1,33 @@ +// +// PosistionIndicator.swift +// +// +// Created by Will Dale on 19/02/2021. +// + +import SwiftUI + +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) + ) + } +} + diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index 8a612207..b4e859ee 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -19,6 +19,8 @@ import SwiftUI public protocol LineAndBarChartData : ChartData { associatedtype CTLineAndBarCS : CTLineAndBarChartStyle + associatedtype XLabels : View + /** Array of strings for the labels on the X Axis instead of the labels in the data points. @@ -79,6 +81,18 @@ public protocol LineAndBarChartData : ChartData { - Tag: getAverage */ func getAverage() -> Double + + /** + Displays a view for the labels on the X Axis. + + Labels can come from either [CTChartDataPoint](x-source-tag://CTChartDataPoint) + or [ChartData](x-source-tag://ChartData) + + - Returns: An `HStack` of `Text` containin x axis labels. + + - Tag: getXAxidLabels + */ + func getXAxisLabels() -> XLabels } // MARK: - Style diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift index 1453c223..aaa702db 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift @@ -7,11 +7,6 @@ import Foundation -extension LineAndBarChartData { - public func getHeaderLocation() -> InfoBoxPlacement { - return self.chartStyle.infoBoxPlacement - } -} extension LineAndBarChartData where Set: SingleDataSet { public func getRange() -> Double { DataFunctions.dataSetRange(from: dataSets) From 4e16bf93f80573a54f99f360ccff655baababdec Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 19 Feb 2021 17:15:48 +0000 Subject: [PATCH 074/152] Update touch interaction. --- .../BarChart/Extras/BarChartEnums.swift | 33 ++++ .../Models/ChartData/BarChartData.swift | 3 - .../ChartData/GroupedBarChartData.swift | 3 - .../ChartData/StackedBarChartData.swift | 3 - .../BarChart/Models/Style/BarChartStyle.swift | 4 +- .../LineChart/Extras/LineChartEnums.swift | 54 ++++++- .../Models/Protocols/LineChartProtocols.swift | 12 +- .../LineChartProtocolsExtensions.swift | 144 +++++++++++------- .../Models/Style/LineChartStyle.swift | 8 +- .../Shared/Extras/SharedEnums.swift | 53 ------- .../Shared/Views/PosistionIndicator.swift | 22 ++- .../{Models => Extras}/LineAndBarEnums.swift | 2 + .../Protocols/LineAndBarProtocols.swift | 8 +- 13 files changed, 212 insertions(+), 137 deletions(-) rename Sources/SwiftUICharts/SharedLineAndBar/{Models => Extras}/LineAndBarEnums.swift (98%) diff --git a/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift b/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift index 631cd72b..a9cb51d9 100644 --- a/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift +++ b/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift @@ -20,3 +20,36 @@ 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 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. + + ``` + + - Tag: BarMarkerType + */ +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/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 45657568..0a6f7c22 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -287,9 +287,6 @@ public final class BarChartData: BarChartDataProtocol { case .vertical: MarkerFull(position: position) .stroke(Color.primary, lineWidth: 2) - case .rectangle: - MarkerFull(position: position) - .stroke(Color.primary, lineWidth: 2) case .full: MarkerFull(position: position) .stroke(Color.primary, lineWidth: 2) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index db32b998..82a0c504 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -343,9 +343,6 @@ public final class GroupedBarChartData: GroupedBarChartDataProtocol { case .vertical: MarkerFull(position: position) .stroke(Color.primary, lineWidth: 2) - case .rectangle: - MarkerFull(position: position) - .stroke(Color.primary, lineWidth: 2) case .full: MarkerFull(position: position) .stroke(Color.primary, lineWidth: 2) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index 91c8cc60..d54f5130 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -189,9 +189,6 @@ public final class StackedBarChartData: GroupedBarChartDataProtocol { case .vertical: MarkerFull(position: position) .stroke(Color.primary, lineWidth: 2) - case .rectangle: - MarkerFull(position: position) - .stroke(Color.primary, lineWidth: 2) case .full: MarkerFull(position: position) .stroke(Color.primary, lineWidth: 2) diff --git a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift index 78f23319..8ca0c754 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift @@ -67,7 +67,7 @@ public struct BarChartStyle: CTBarChartStyle { public var infoBoxPlacement : InfoBoxPlacement public var infoBoxValueColour : Color public var infoBoxDescriptionColor : Color - public var markerType : MarkerType + public var markerType : BarMarkerType public var xAxisGridStyle : GridStyle public var xAxisLabelPosition : XAxisLabelPosistion @@ -103,7 +103,7 @@ public struct BarChartStyle: CTBarChartStyle { public init(infoBoxPlacement : InfoBoxPlacement = .floating, infoBoxValueColour : Color = Color.primary, infoBoxDescriptionColor : Color = Color.primary, - markerType : MarkerType = .full(attachment: .line), + markerType : BarMarkerType = .full, xAxisGridStyle : GridStyle = GridStyle(), xAxisLabelPosition : XAxisLabelPosistion = .bottom, diff --git a/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift b/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift index 9a20cfe1..cc81408b 100644 --- a/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift +++ b/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift @@ -91,7 +91,59 @@ public enum PointShape { */ public enum MarkerAttachemnt { /// Attached to the line. - case 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 // Rounded rectangle. + case vertical // Vertical line from top to bottom. + case full // 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. + + ``` + + - Tag: LineMarkerType + */ +public enum LineMarkerType: MarkerType { + /// No overlay markers. + case none + /// Rounded rectangle. + 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. + ``` + + - Tag: Dot + */ +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/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index 2bef700b..46735f3f 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -20,7 +20,7 @@ import SwiftUI */ public protocol LineChartDataProtocol: LineAndBarChartData { - associatedtype Bob : View + associatedtype Marker : View /** Whether it is a normal or filled line. */ @@ -40,10 +40,7 @@ public protocol LineChartDataProtocol: LineAndBarChartData { func getSinglePoint(touchLocation: CGPoint, chartSize: GeometryProxy, dataSet: LineDataSet) -> CGPoint - func markerSubView(dataSet : LineDataSet, - touchLocation : CGPoint, - chartSize : GeometryProxy - ) -> Bob + func markerSubView(dataSet: LineDataSet, touchLocation: CGPoint, chartSize: GeometryProxy) -> Marker } @@ -61,11 +58,6 @@ public protocol CTLineChartStyle : CTLineAndBarChartStyle { [See Baseline](x-source-tag://Baseline) */ var baseline: Baseline { get set } - - /** - Where the Y and X touch markers should attach themselves to. - */ - var markerAttachemnt : MarkerAttachemnt { get set } } diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index fbfdb560..44636d48 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -262,48 +262,39 @@ extension LineChartDataProtocol { } return .zero } - + @ViewBuilder public func markerSubView(dataSet : LineDataSet, touchLocation : CGPoint, chartSize : GeometryProxy ) -> some View { - switch self.chartStyle.markerType { + switch self.chartStyle.markerType as! LineMarkerType { case .none: EmptyView() - case .rectangle: - - let position : CGPoint = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + 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.frame(in: .global), dataPoints: dataSet.dataPoints, touchLocation: touchLocation, - lineType: dataSet.style.lineType) - RoundedRectangle(cornerRadius: 25.0, style: .continuous) - .fill(Color.clear) - .frame(width: 100, height: chartSize.frame(in: .local).height) - .position(x: position.x, - y: chartSize.frame(in: .local).midY) - .overlay( - RoundedRectangle(cornerRadius: 25.0, style: .continuous) - .stroke(Color.primary, lineWidth: 2) - .shadow(color: .primary, radius: 4, x: 0, y: 0) - .frame(width: 50, height: chartSize.frame(in: .local).height) - .position(x: position.x, - y: chartSize.frame(in: .local).midY) - ) - - PosistionIndicator() - .frame(width: 15, height: 15) - .position(position) + lineType: dataSet.style.lineType)) case .vertical(attachment: let attach): switch attach { - case .line: - Vertical(position: self.getIndicatorLocation(rect: chartSize.frame(in: .global), - dataPoints: dataSet.dataPoints, - touchLocation: touchLocation, - lineType: dataSet.style.lineType)) + case .line(dot: let indicator): + + let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + dataPoints: dataSet.dataPoints, + touchLocation: touchLocation, + lineType: dataSet.style.lineType) + + Vertical(position: position) .stroke(Color.primary, lineWidth: 2) + + IndicatorSwitch(indicator: indicator, location: position) + case .point: Vertical(position: self.getSinglePoint(touchLocation: touchLocation, chartSize: chartSize, @@ -314,12 +305,18 @@ extension LineChartDataProtocol { case .full(attachment: let attach): switch attach { - case .line: - MarkerFull(position: self.getIndicatorLocation(rect: chartSize.frame(in: .global), - dataPoints: dataSet.dataPoints, - touchLocation: touchLocation, - lineType: dataSet.style.lineType)) + case .line(dot: let indicator): + + let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + dataPoints: dataSet.dataPoints, + touchLocation: touchLocation, + lineType: dataSet.style.lineType) + + MarkerFull(position: position) .stroke(Color.primary, lineWidth: 2) + + IndicatorSwitch(indicator: indicator, location: position) + case .point: MarkerFull(position: self.getSinglePoint(touchLocation: touchLocation, chartSize: chartSize, @@ -330,12 +327,18 @@ extension LineChartDataProtocol { case .bottomLeading(attachment: let attach): switch attach { - case .line: - MarkerBottomLeading(position: self.getIndicatorLocation(rect: chartSize.frame(in: .global), - dataPoints: dataSet.dataPoints, - touchLocation: touchLocation, - lineType: dataSet.style.lineType)) + case .line(dot: let indicator): + + let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + dataPoints: dataSet.dataPoints, + touchLocation: touchLocation, + lineType: dataSet.style.lineType) + + MarkerBottomLeading(position: position) .stroke(Color.primary, lineWidth: 2) + + IndicatorSwitch(indicator: indicator, location: position) + case .point: MarkerBottomLeading(position: self.getSinglePoint(touchLocation: touchLocation, chartSize: chartSize, @@ -346,12 +349,18 @@ extension LineChartDataProtocol { case .bottomTrailing(attachment: let attach): switch attach { - case .line: - MarkerBottomTrailing(position: self.getIndicatorLocation(rect: chartSize.frame(in: .global), - dataPoints: dataSet.dataPoints, - touchLocation: touchLocation, - lineType: dataSet.style.lineType)) + case .line(dot: let indicator): + + let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + dataPoints: dataSet.dataPoints, + touchLocation: touchLocation, + lineType: dataSet.style.lineType) + + MarkerBottomTrailing(position: position) .stroke(Color.primary, lineWidth: 2) + + IndicatorSwitch(indicator: indicator, location: position) + case .point: MarkerBottomTrailing(position: self.getSinglePoint(touchLocation: touchLocation, chartSize: chartSize, @@ -362,12 +371,18 @@ extension LineChartDataProtocol { case .topLeading(attachment: let attach): switch attach { - case .line: - MarkerTopLeading(position: self.getIndicatorLocation(rect: chartSize.frame(in: .global), - dataPoints: dataSet.dataPoints, - touchLocation: touchLocation, - lineType: dataSet.style.lineType)) + case .line(dot: let indicator): + + let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + dataPoints: dataSet.dataPoints, + touchLocation: touchLocation, + lineType: dataSet.style.lineType) + + MarkerTopLeading(position: position) .stroke(Color.primary, lineWidth: 2) + + IndicatorSwitch(indicator: indicator, location: position) + case .point: MarkerTopLeading(position: self.getSinglePoint(touchLocation: touchLocation, chartSize: chartSize, @@ -378,12 +393,18 @@ extension LineChartDataProtocol { case .topTrailing(attachment: let attach): switch attach { - case .line: - MarkerTopTrailing(position: self.getIndicatorLocation(rect: chartSize.frame(in: .global), - dataPoints: dataSet.dataPoints, - touchLocation: touchLocation, - lineType: dataSet.style.lineType)) + case .line(dot: let indicator): + + let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + dataPoints: dataSet.dataPoints, + touchLocation: touchLocation, + lineType: dataSet.style.lineType) + + MarkerTopTrailing(position: position) .stroke(Color.primary, lineWidth: 2) + + IndicatorSwitch(indicator: indicator, location: position) + case .point: MarkerTopTrailing(position: self.getSinglePoint(touchLocation: touchLocation, chartSize: chartSize, @@ -394,3 +415,20 @@ extension LineChartDataProtocol { } } + +struct IndicatorSwitch: View { + + let indicator: Dot + let location : CGPoint + + 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) + } + } + +} diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift index e0fe941c..4d65c6eb 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift @@ -72,8 +72,7 @@ public struct LineChartStyle: CTLineChartStyle { public var infoBoxPlacement : InfoBoxPlacement public var infoBoxValueColour : Color public var infoBoxDescriptionColor : Color - public var markerType : MarkerType - public var markerAttachemnt : MarkerAttachemnt + public var markerType : LineMarkerType public var xAxisGridStyle : GridStyle public var xAxisLabelPosition : XAxisLabelPosistion @@ -95,7 +94,6 @@ public struct LineChartStyle: CTLineChartStyle { /// - infoBoxDescriptionColor: Colour of the description part of the touch info. /// /// - markerType: Where the marker lines come from to meet at a specified point. - /// - markerAttachemnt: Where the Y and X touch markers should attach themselves to. /// /// - xAxisGridStyle: Style of the vertical lines breaking up the chart. /// - xAxisLabelPosition: Location of the X axis labels - Top or Bottom. @@ -113,8 +111,7 @@ public struct LineChartStyle: CTLineChartStyle { infoBoxValueColour : Color = Color.primary, infoBoxDescriptionColor : Color = Color.primary, - markerType : MarkerType = .rectangle, - markerAttachemnt : MarkerAttachemnt = .line, + markerType : LineMarkerType = .indicator(style: DotStyle()), xAxisGridStyle : GridStyle = GridStyle(), xAxisLabelPosition : XAxisLabelPosistion = .bottom, @@ -134,7 +131,6 @@ public struct LineChartStyle: CTLineChartStyle { self.infoBoxDescriptionColor = infoBoxDescriptionColor self.markerType = markerType - self.markerAttachemnt = markerAttachemnt self.xAxisGridStyle = xAxisGridStyle self.xAxisLabelPosition = xAxisLabelPosition diff --git a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift index 139de316..fa675d74 100644 --- a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift @@ -80,56 +80,3 @@ public enum InfoBoxPlacement { /// Fix in the Header box. Must have .headerBox(). case header } - - -/** - Where the marker lines come from to meet at a specified point. - ``` - case none // No overlay markers. - case rectangle // Rounded rectangle. - case vertical // Vertical line from top to bottom. - case full // 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. - - ``` - - - Tag: MarkerType - */ -public enum MarkerType { - /// No overlay markers. - case none - /// Rounded rectangle. - case rectangle - /// 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) -} - -public enum BarMarkerType { - /// 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/Shared/Views/PosistionIndicator.swift b/Sources/SwiftUICharts/Shared/Views/PosistionIndicator.swift index c319fe41..88291783 100644 --- a/Sources/SwiftUICharts/Shared/Views/PosistionIndicator.swift +++ b/Sources/SwiftUICharts/Shared/Views/PosistionIndicator.swift @@ -13,8 +13,8 @@ internal struct PosistionIndicator: View { private let lineColour : Color private let lineWidth : CGFloat - internal init(fillColour : Color = Color.primary, - lineColour : Color = Color.blue, + internal init(fillColour : Color = Color.primary, + lineColour : Color = Color.blue, lineWidth : CGFloat = 3 ) { self.fillColour = fillColour @@ -31,3 +31,21 @@ internal struct PosistionIndicator: View { } } +public struct DotStyle { + + let size : CGFloat + let fillColour : Color + let lineColour : Color + let lineWidth : CGFloat + + 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/SharedLineAndBar/Models/LineAndBarEnums.swift b/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift similarity index 98% rename from Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarEnums.swift rename to Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift index f7df2bd3..019e9b09 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/LineAndBarEnums.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift @@ -64,6 +64,8 @@ public enum YAxisLabelPosistion { case yAxis(specifier: String) // Places the label in the yAxis labels. case center(specifier: String) // Places the label in the center of chart. ``` + + - Tag: DisplayValue */ public enum DisplayValue { /// No label. diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index b4e859ee..61300470 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -95,18 +95,24 @@ public protocol LineAndBarChartData : ChartData { func getXAxisLabels() -> XLabels } + // MARK: - Style +public protocol MarkerType {} + /** A protocol to extend functionality of `CTChartStyle` specifically for Line and Bar Charts. - Tag: CTLineAndBarChartStyle */ public protocol CTLineAndBarChartStyle: CTChartStyle { + + associatedtype Mark : MarkerType + /** Where the marker lines come from to meet at a specified point. */ - var markerType : MarkerType { get set } + var markerType : Mark { get set } /** Style of the vertical lines breaking up the chart From cdadce7d985b6d5d2cdc8dd51e6d8b540a2d832c Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 21 Feb 2021 08:41:15 +0000 Subject: [PATCH 075/152] Remove forced downcast. --- .../Models/Protocols/LineChartProtocolsExtensions.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index 44636d48..19d7a981 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -262,13 +262,14 @@ extension LineChartDataProtocol { } return .zero } - +} +extension LineChartDataProtocol where Self.CTLineAndBarCS.Mark == LineMarkerType { @ViewBuilder public func markerSubView(dataSet : LineDataSet, touchLocation : CGPoint, chartSize : GeometryProxy ) -> some View { - switch self.chartStyle.markerType as! LineMarkerType { + switch self.chartStyle.markerType { case .none: EmptyView() case .indicator(let style): @@ -412,7 +413,6 @@ extension LineChartDataProtocol { .stroke(Color.primary, lineWidth: 2) } } - } } From 972c984c997dcba084db6a90a3d8fbb8151c20bc Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 21 Feb 2021 08:42:20 +0000 Subject: [PATCH 076/152] Move Legend functions to internal protocol. --- .../Models/ChartData/BarChartData.swift | 8 +++-- .../ChartData/GroupedBarChartData.swift | 8 +++-- .../ChartData/StackedBarChartData.swift | 10 +++++-- .../Models/ChartData/LineChartData.swift | 25 ++++++++++++---- .../Models/ChartData/MultiLineChartData.swift | 30 +++++++++++++++++-- 5 files changed, 68 insertions(+), 13 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 0a6f7c22..2aec1c2a 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -167,7 +167,7 @@ import SwiftUI - Tag: BarChartData */ -public final class BarChartData: BarChartDataProtocol { +public final class BarChartData: BarChartDataProtocol, LegendProtocol { // MARK: - Properties public let id : UUID = UUID() @@ -308,7 +308,7 @@ public final class BarChartData: BarChartDataProtocol { } // MARK: - Legends - public func setupLegends() { + internal func setupLegends() { switch self.barStyle.colourFrom { case .barStyle: @@ -387,6 +387,10 @@ public final class BarChartData: BarChartDataProtocol { } } + internal func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } + public typealias Set = BarDataSet public typealias DataPoint = BarChartDataPoint public typealias CTStyle = BarChartStyle diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 82a0c504..b0d16322 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -175,7 +175,7 @@ import SwiftUI - Tag: GroupedBarChartData */ -public final class GroupedBarChartData: GroupedBarChartDataProtocol { +public final class GroupedBarChartData: GroupedBarChartDataProtocol, LegendProtocol { // MARK: - Properties public let id : UUID = UUID() @@ -364,7 +364,7 @@ public final class GroupedBarChartData: GroupedBarChartDataProtocol { } // MARK: - Legends - public func setupLegends() { + internal func setupLegends() { for group in self.groups { @@ -403,6 +403,10 @@ public final class GroupedBarChartData: GroupedBarChartDataProtocol { } } + internal func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } + public typealias Set = GroupedBarDataSets public typealias DataPoint = GroupedBarChartDataPoint public typealias CTStyle = BarChartStyle diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index d54f5130..e11616b5 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -7,7 +7,7 @@ import SwiftUI -public final class StackedBarChartData: GroupedBarChartDataProtocol { +public final class StackedBarChartData: GroupedBarChartDataProtocol, LegendProtocol { // MARK: - Properties public let id : UUID = UUID() @@ -210,7 +210,7 @@ public final class StackedBarChartData: GroupedBarChartDataProtocol { } // MARK: - Legends - public func setupLegends() { + internal func setupLegends() { for group in self.groups { if group.colourType == .colour, @@ -247,6 +247,12 @@ public final class StackedBarChartData: GroupedBarChartDataProtocol { } } } + + internal func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } + + public typealias Set = GroupedBarDataSets public typealias DataPoint = GroupedBarChartDataPoint public typealias CTStyle = BarChartStyle diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index 6f80f483..8ec112db 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -153,7 +153,7 @@ import SwiftUI - Tag: LineChartData */ -public final class LineChartData: LineChartDataProtocol { +public final class LineChartData: LineChartDataProtocol, LegendProtocol { // MARK: - Properties public let id : UUID = UUID() @@ -169,8 +169,8 @@ public final class LineChartData: LineChartDataProtocol { public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) - - // MARK: - Initializers + + // MARK: - Initializer /// Initialises a Single Line Chart. /// /// - Parameters: @@ -195,6 +195,7 @@ public final class LineChartData: LineChartDataProtocol { self.chartType = (chartType: .line, dataSetType: .single) self.setupLegends() } + // , calc : @escaping (LineDataSet) -> LineDataSet // MARK: - Labels public func getXAxisLabels() -> some View { @@ -241,6 +242,16 @@ public final class LineChartData: LineChartDataProtocol { } } + + // MARK: - Points + public func getPointMarker() -> some View { + PointsSubView(dataSets : dataSets, + minValue : self.getMinValue(), + range : self.getRange(), + animation : self.chartStyle.globalAnimation, + isFilled : self.isFilled) + } + // MARK: - Touch public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [LineChartDataPoint] { var points : [LineChartDataPoint] = [] @@ -274,7 +285,7 @@ public final class LineChartData: LineChartDataProtocol { } // MARK: - Legends - public func setupLegends() { + internal func setupLegends() { if dataSets.style.colourType == .colour, let colour = dataSets.style.colour @@ -311,7 +322,7 @@ public final class LineChartData: LineChartDataProtocol { chartType : .line)) } } - + // MARK: - Data Functions public func getRange() -> Double { switch self.chartStyle.baseline { @@ -334,6 +345,10 @@ public final class LineChartData: LineChartDataProtocol { } } + internal func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } + public typealias Set = LineDataSet public typealias DataPoint = LineChartDataPoint } diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index 308ad2f2..0a7b6219 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -166,7 +166,7 @@ import SwiftUI - Tag: LineChartData */ -public final class MultiLineChartData: LineChartDataProtocol { +public final class MultiLineChartData: LineChartDataProtocol, LegendProtocol { // MARK: - Properties public let id : UUID = UUID() @@ -254,6 +254,17 @@ public final class MultiLineChartData: LineChartDataProtocol { } } + // MARK: - Points + public func getPointMarker() -> some View { + ForEach(self.dataSets.dataSets, id: \.self) { dataSet in + PointsSubView(dataSets : dataSet, + minValue : self.getMinValue(), + range : self.getRange(), + animation : self.chartStyle.globalAnimation, + isFilled : self.isFilled) + } + } + // MARK: - Touch public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [LineChartDataPoint] { var points : [LineChartDataPoint] = [] @@ -292,9 +303,19 @@ public final class MultiLineChartData: LineChartDataProtocol { } } } + +// public func getPointMarker() -> some View { +// ForEach(self.dataSets.dataSets, id: \.self) { dataSet in +// PointsSubView(dataSets : dataSet, +// minValue : self.getMinValue(), +// range : self.getRange(), +// animation : self.chartStyle.globalAnimation, +// isFilled : self.isFilled) +// } +// } // MARK: - Legends - public func setupLegends() { + internal func setupLegends() { for dataSet in dataSets.dataSets { if dataSet.style.colourType == .colour, let colour = dataSet.style.colour @@ -355,6 +376,11 @@ public final class MultiLineChartData: LineChartDataProtocol { } } + internal func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } + + public typealias Set = MultiLineDataSet public typealias DataPoint = LineChartDataPoint } From 059149749f9ae5befa45e70d60081cb009cd5e7d Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 21 Feb 2021 08:44:04 +0000 Subject: [PATCH 077/152] Refactor Point Markers into protocol to remove forced downcast. --- .../Models/Protocols/LineChartProtocols.swift | 5 ++++- .../ViewModifiers/PointMarkers.swift | 22 +------------------ 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index 46735f3f..3d41e6ce 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -21,6 +21,8 @@ import SwiftUI public protocol LineChartDataProtocol: LineAndBarChartData { associatedtype Marker : View + associatedtype Points : View + /** Whether it is a normal or filled line. */ @@ -41,10 +43,11 @@ public protocol LineChartDataProtocol: LineAndBarChartData { func getSinglePoint(touchLocation: CGPoint, chartSize: GeometryProxy, dataSet: LineDataSet) -> CGPoint func markerSubView(dataSet: LineDataSet, touchLocation: CGPoint, chartSize: GeometryProxy) -> Marker + + func getPointMarker() -> Points } - // MARK: - Style /** A protocol to extend functionality of `CTLineAndBarChartStyle` specifically for Line Charts. diff --git a/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift index 4dc18a6c..ddbb1fd3 100644 --- a/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift @@ -23,27 +23,7 @@ internal struct PointMarkers: ViewModifier where T: LineChartDataProtocol { ZStack { if chartData.isGreaterThanTwo() { content - - if chartData.chartType.dataSetType == .single { - - let data = chartData as! LineChartData - PointsSubView(dataSets: data.dataSets, - minValue: minValue, - range: range, - animation: chartData.chartStyle.globalAnimation, - isFilled: chartData.isFilled) - - } else if chartData.chartType.dataSetType == .multi { - - let data = chartData as! MultiLineChartData - ForEach(data.dataSets.dataSets, id: \.self) { dataSet in - PointsSubView(dataSets: dataSet, - minValue: minValue, - range: range, - animation: chartData.chartStyle.globalAnimation, - isFilled: chartData.isFilled) - } - } + chartData.getPointMarker() } else { content } } } From 046a0cbb11f0c5ac0009b96d1be431b482d9e8ad Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 21 Feb 2021 08:45:54 +0000 Subject: [PATCH 078/152] Refactor Legend functions into internal protocol. --- .../Models/Doughnut/DoughnutChartData.swift | 6 ++- .../PieChart/Models/Pie/PieChartData.swift | 6 ++- .../Models/Protocols/SharedProtocols.swift | 46 +++++++++++-------- .../Protocols/SharedProtocolsExtensions.swift | 7 --- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift index bcbc391e..f4e60c74 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift @@ -7,7 +7,7 @@ import SwiftUI -public final class DoughnutChartData: DoughnutChartDataProtocol { +public final class DoughnutChartData: DoughnutChartDataProtocol, LegendProtocol { @Published public var id : UUID = UUID() @Published public var dataSets : PieDataSet @@ -39,6 +39,10 @@ public final class DoughnutChartData: DoughnutChartDataProtocol { public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } + internal func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } + public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint public typealias CTStyle = DoughnutChartStyle diff --git a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift index 294392ce..2efbd6ef 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift @@ -7,7 +7,7 @@ import SwiftUI -public final class PieChartData: PieChartDataProtocol { +public final class PieChartData: PieChartDataProtocol, LegendProtocol { @Published public var id : UUID = UUID() @Published public var dataSets : PieDataSet @@ -39,6 +39,10 @@ public final class PieChartData: PieChartDataProtocol { public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } + internal func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } + public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint public typealias CTStyle = PieChartStyle diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 9d173561..2864bc18 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -80,29 +80,13 @@ public protocol ChartData: ObservableObject, Identifiable { */ var chartType: (chartType: ChartType, dataSetType: DataSetType) { get } - /** - Sets the order the Legends are layed out in. - - Returns: Ordered array of Legends. - - # Reference - [LegendData](x-source-tag://LegendData) - - - Tag: legendOrder - */ - func legendOrder() -> [LegendData] - - /** - Configures the legends based on the type of chart. - - - Tag: setupLegends - */ - func setupLegends() - + /** Returns whether there are two or more dataPoints */ func isGreaterThanTwo() -> Bool + // MARK: Touch /** Gets the nearest data points to the touch location. @@ -136,8 +120,34 @@ public protocol ChartData: ObservableObject, Identifiable { - Returns: The relevent view for the chart type and options. */ func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> Touch + + } +internal protocol LegendProtocol { + + /** + Sets the order the Legends are layed out in. + - Returns: Ordered array of Legends. + + # Reference + [LegendData](x-source-tag://LegendData) + + - Tag: legendOrder + */ + func legendOrder() -> [LegendData] + + /** + Configures the legends based on the type of chart. + + - Tag: setupLegends + */ + func setupLegends() +} + + + + // MARK: - Data Sets /** Main protocol set conformace for types of Data Sets. diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift index dd3a39c9..f900ecfc 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift @@ -7,13 +7,6 @@ import Foundation -// MARK: Chart Data -extension ChartData { - public func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } -} - extension ChartData where Set: SingleDataSet { public func isGreaterThanTwo() -> Bool { return dataSets.dataPoints.count > 2 From 2909851893bba30b55c40847ce915f0e06ad36bf Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 21 Feb 2021 08:47:03 +0000 Subject: [PATCH 079/152] Fix animations. --- .../SharedLineAndBar/ViewModifiers/YAxisPOI.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index e53de26f..6a484a78 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -129,10 +129,9 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { .overlay(DiamondShape() .stroke(lineColour, style: strokeStyle) ) - .position(x: startAnimation ? geo.size.width / 2 : 0, + .position(x: geo.size.width / 2, y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo)) .opacity(startAnimation ? 1 : 0) - .animation(chartData.chartStyle.globalAnimation.speed(2)) } } } From 5c9b0a4a1488e33f310d98970521cf991d4adaf2 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 21 Feb 2021 08:47:42 +0000 Subject: [PATCH 080/152] Make setupLegends function internal. --- Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift index dff600ba..93fcdc46 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift @@ -73,7 +73,7 @@ extension PieAndDoughnutChartDataProtocol where Set == PieDataSet { return [HashablePoint(x: touchLocation.x, y: touchLocation.y)] } - public func setupLegends() { + internal func setupLegends() { for data in dataSets.dataPoints { if let legend = data.pointDescription { self.legends.append(LegendData(id : data.id, From ac72e2e9074e428415a23a194e0292ccb75e3dad Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 21 Feb 2021 09:15:44 +0000 Subject: [PATCH 081/152] Change infobox font. --- .../Shared/ViewModifiers/InfoBox.swift | 19 +++++++++++++++++-- .../Shared/Views/TouchOverlayBox.swift | 6 +++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift index 7e960b2b..7b3e72e7 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift @@ -47,16 +47,31 @@ internal struct InfoBox: ViewModifier where T: ChartData { ForEach(chartData.infoView.touchOverlayInfo, id: \.self) { point in HStack { Text("\(point.value, specifier: chartData.infoView.touchSpecifier)") - .font(.body) + .font(.subheadline) .foregroundColor(chartData.chartStyle.infoBoxValueColour) if let label = point.pointDescription { Text(label) - .font(.body) + .font(.subheadline) .foregroundColor(chartData.chartStyle.infoBoxDescriptionColor) } } } }.frame(height: 40) + .padding(.horizontal, 6) + .background( + Group { + if chartData.infoView.isTouchCurrent { + RoundedRectangle(cornerRadius: 5.0, style: .continuous) + .fill(Color.systemsBackground) + .overlay( + Group { + RoundedRectangle(cornerRadius: 5.0) + .stroke(Color.primary, lineWidth: 1) + } + ) + } + } + ) } diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index fea57195..bafe5c89 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -43,16 +43,16 @@ internal struct TouchOverlayBox: View { ForEach(selectedPoints, id: \.self) { point in if ignoreZero && point.value != 0 { Text("\(point.value, specifier: specifier)") - .font(.body) + .font(.subheadline) .foregroundColor(valueColour) } else if !ignoreZero { Text("\(point.value, specifier: specifier)") - .font(.body) + .font(.subheadline) .foregroundColor(valueColour) } if let label = point.pointDescription { Text(label) - .font(.body) + .font(.subheadline) .foregroundColor(descriptionColour) } } From 3ed40a5807da4dcdea8005e93930352d20ce8298 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 21 Feb 2021 14:13:36 +0000 Subject: [PATCH 082/152] Add single layer of multilayer pie chart. --- .../MultiLayer/MultiLayerPieChartData.swift | 163 ++++++++---------- .../PieChart/Models/PieChartDataPoint.swift | 3 - .../PieChart/Models/PieChartProtocols.swift | 82 ++++++--- 3 files changed, 134 insertions(+), 114 deletions(-) diff --git a/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift index 458d3822..d0602776 100644 --- a/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift @@ -7,157 +7,140 @@ import SwiftUI -public final class MultiLayerPieChartData { - +public final class MultiLayerPieChartData: MultiPieChartDataProtocol { + @Published public var id : UUID = UUID() @Published public var dataSets : MultiPieDataSet @Published public var metadata : ChartMetadata @Published public var chartStyle : PieChartStyle @Published public var legends : [LegendData] -// @Published public var infoView : InfoViewData - + @Published public var infoView : InfoViewData + public var noDataText: Text public var chartType: (chartType: ChartType, dataSetType: DataSetType) - + public init(dataSets : MultiPieDataSet, - metadata : ChartMetadata = ChartMetadata(), + metadata : ChartMetadata, chartStyle : PieChartStyle = PieChartStyle(), noDataText : Text ) { - self.dataSets = dataSets + self.dataSets = dataSets self.metadata = metadata self.chartStyle = chartStyle self.legends = [LegendData]() -// self.infoView = InfoViewData() + self.infoView = InfoViewData() self.noDataText = noDataText - self.chartType = (chartType: .pie, dataSetType: .multi) + self.chartType = (chartType: .pie, dataSetType: .single) + // self.setupLegends() + self.makeDataPoints() + } + + public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } + + + + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [MultiPieDataPoint] { + let points : [MultiPieDataPoint] = [] + return points + } + + public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { + return [HashablePoint(x: touchLocation.x, y: touchLocation.y)] + } + + internal func setupLegends() {} + + internal func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} } + public typealias Set = MultiPieDataSet + public typealias DataPoint = MultiPieDataPoint + public typealias CTStyle = PieChartStyle } -public struct MultiPieDataSet: Hashable, Identifiable { - public let id : UUID - public var dataPoints : [MultiPieDataPoint] { - didSet { - let start = dataPoints.first?.startAngle ?? 0 - let amount = dataPoints.last?.amount ?? 0 - let end = start + amount - segmentWidth = end - } - } - - var segmentWidth : Double? - /// Initialises a new data set for Multiline Line Chart. +// MARK: - Data Sets +public struct MultiPieDataSet: SingleDataSet { + + public var id: UUID = UUID() + public var dataPoints : [MultiPieDataPoint] + public init(dataPoints: [MultiPieDataPoint]) { - self.id = UUID() self.dataPoints = dataPoints } + + public typealias DataPoint = MultiPieDataPoint + } -public struct MultiPieDataPoint: Hashable, Identifiable { + + +// MARK: - Data Point +public struct MultiPieDataPoint: CTPieDataPoint { - public var id : UUID = UUID() + public var id: UUID = UUID() + // CTPieDataPoint + public var startAngle : Double = 0 + public var amount : Double = 0 + // CTChartDataPoint public var value : Double - public var xAxisLabel : String? public var pointDescription : String? public var date : Date? + public var colour : Color - public var dataSets : MultiPieDataSet? + public var layerDataPoints : [MultiPieDataPoint]? - var startAngle : Double = 0 - var amount : Double = 0 - public init(value : Double, - xAxisLabel : String? = nil, pointDescription: String? = nil, date : Date? = nil, colour : Color = Color.red, - dataSets : MultiPieDataSet? = nil + layerDataPoints : [MultiPieDataPoint]? = nil ) { self.value = value - self.xAxisLabel = xAxisLabel self.pointDescription = pointDescription self.date = date self.colour = colour - self.dataSets = dataSets + self.layerDataPoints = layerDataPoints } + } // MARK: - View -public struct MultiLayerPieChart: View { +public struct MultiLayerPie: View where ChartData: MultiLayerPieChartData { - let chartData : MultiLayerPieChartData = makeData() - - public init() {} + @ObservedObject var chartData: ChartData + + public init(chartData: ChartData) { + self.chartData = chartData + } public var body: some View { ZStack { - - ForEach(chartData.dataSets.dataPoints, id: \.self) { dataPoint in + ForEach(chartData.dataSets.dataPoints, id: \.self) { data in + PieSegmentShape(id: data.id, + startAngle: data.startAngle, + amount: data.amount) + .fill(data.colour) -// PieSegmentShape(id: dataPoint.id, -// startAngle: dataPoint.startAngle, -// amount: dataPoint.amount) -// .fill(dataPoint.colour) - - if let bob = dataPoint.dataSets { - - ForEach(bob.dataPoints) { point in - DoughnutSegmentShape(id : UUID(), - startAngle : point.startAngle, - amount : point.amount) - .strokeBorder(point.colour, - lineWidth: 30) + if let points = data.layerDataPoints { + ForEach(points, id: \.self) { point in + DoughnutSegmentShape(id: point.id, + startAngle: point.startAngle, + amount: point.amount) + .strokeBorder(point.colour, lineWidth: 60) } } - } } } } - -extension MultiLayerPieChart { - static func makeData() -> MultiLayerPieChartData { - - let data = MultiPieDataSet(dataPoints: [ - MultiPieDataPoint( - value: 10, - colour: Color(.gray), - dataSets: MultiPieDataSet(dataPoints: [ - MultiPieDataPoint(value: 20, - colour: .red), - MultiPieDataPoint(value: 20, - colour: .green)])), - - MultiPieDataPoint( - value: 40, - colour: Color(.darkGray), - dataSets: MultiPieDataSet(dataPoints: [ - MultiPieDataPoint(value: 20, - colour: .blue), - MultiPieDataPoint(value: 20, - colour: Color(.cyan))])), - MultiPieDataPoint( - value: 20, - colour: Color(.gray), - dataSets: MultiPieDataSet(dataPoints: [ - MultiPieDataPoint(value: 20, - colour: Color(.yellow)), - MultiPieDataPoint(value: 20, - colour: Color(.magenta))]) - )]) - - return MultiLayerPieChartData(dataSets: data, - noDataText: Text("Bob")) - } -} diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartDataPoint.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartDataPoint.swift index abff4c7a..747fcb09 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartDataPoint.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartDataPoint.swift @@ -11,7 +11,6 @@ public struct PieChartDataPoint: CTPieDataPoint { public var id : UUID = UUID() public var value : Double - public var xAxisLabel : String? public var pointDescription : String? public var date : Date? @@ -21,13 +20,11 @@ public struct PieChartDataPoint: CTPieDataPoint { public var amount : Double = 0 public init(value : Double, - xAxisLabel : String? = nil, pointDescription: String? = nil, date : Date? = nil, colour : Color = Color.red ) { self.value = value - self.xAxisLabel = xAxisLabel self.pointDescription = pointDescription self.date = date self.colour = colour diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift index 93fcdc46..1249b34e 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift @@ -38,14 +38,49 @@ public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol {} */ public protocol DoughnutChartDataProtocol : PieAndDoughnutChartDataProtocol {} +public protocol MultiPieChartDataProtocol : PieAndDoughnutChartDataProtocol {} + -public protocol CTMultiPieChartDataPoints: CTChartDataPoint {} +// MARK: - DataSet public protocol CTMultiPieDataSet: DataSet {} -// MARK: - Pie and Doughnut -extension PieAndDoughnutChartDataProtocol where Set == PieDataSet { +extension PieAndDoughnutChartDataProtocol where Set == MultiPieDataSet, DataPoint == MultiPieDataPoint { + 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].startAngle = startAngle + self.dataSets.dataPoints[point].amount = amount + + + let layerTotal = self.dataSets.dataPoints[point].layerDataPoints?.reduce(0) { $0 + $1.value } ?? 0 + var layerStartAngle = startAngle + + self.dataSets.dataPoints[point].layerDataPoints?.indices.forEach { (layer) in + + let layerValue = self.dataSets.dataPoints[point].layerDataPoints?[layer].value ?? 0 + let layerAmount = amount * (layerValue / layerTotal) + + self.dataSets.dataPoints[point].layerDataPoints?[layer].startAngle = layerStartAngle + self.dataSets.dataPoints[point].layerDataPoints?[layer].amount = layerAmount + + + layerStartAngle += layerAmount + + } + startAngle += amount + } + } +} + +// * (180 / Double.pi) + +extension PieAndDoughnutChartDataProtocol where Set == PieDataSet, DataPoint == PieChartDataPoint { internal func makeDataPoints() { let total = self.dataSets.dataPoints.reduce(0) { $0 + $1.value } @@ -102,7 +137,29 @@ extension PieAndDoughnutChartDataProtocol where Set == PieDataSet { } } -// MARK: Style + + + +// MARK: - DataPoints + +/** + A protocol to extend functionality of `CTChartDataPoint` specifically for Pie and Doughnut Charts. + + Currently empty. + + - Tag: CTPieDataPoint + */ +public protocol CTPieDataPoint: CTChartDataPoint { + var startAngle : Double { get set } + var amount : Double { get set } +} + +public protocol CTMultiPieChartDataPoints: CTChartDataPoint {} + + + + +// MARK: - Style /** A protocol to extend functionality of `CTChartStyle` specifically for Pie and Doughnut Charts. @@ -135,20 +192,3 @@ public protocol CTDoughnutChartStyle: CTPieAndDoughnutChartStyle { */ var strokeWidth: CGFloat { get set } } - - -// MARK: DataPoints - -/** - A protocol to extend functionality of `CTChartDataPoint` specifically for Pie and Doughnut Charts. - - Currently empty. - - - Tag: CTPieDataPoint - */ -public protocol CTPieDataPoint: CTChartDataPoint { - var startAngle : Double { get set } - var amount : Double { get set } -} - - From 5481408b9d4ec74b28f0401c4c35893190ff5302 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Mon, 22 Feb 2021 09:19:54 +0000 Subject: [PATCH 083/152] Make Multi part pie chart work. --- .../Models/DataPoints/MultiPieDataPoint.swift | 37 ++++++++ .../{ => DataPoints}/PieChartDataPoint.swift | 0 .../Models/DataSets/MultiPieDataSet.swift | 20 +++++ .../Models/{ => DataSets}/PieDataSet.swift | 0 .../MultiLayer/MultiLayerPieChartData.swift | 84 ------------------- .../PieChart/Models/PieChartProtocols.swift | 39 +++++++-- .../PieChart/Shapes/PieSegmentShape.swift | 2 + .../PieChart/Views/DoughnutChart.swift | 2 +- .../PieChart/Views/MultiLayerPie.swift | 61 ++++++++++++++ 9 files changed, 154 insertions(+), 91 deletions(-) create mode 100644 Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift rename Sources/SwiftUICharts/PieChart/Models/{ => DataPoints}/PieChartDataPoint.swift (100%) create mode 100644 Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift rename Sources/SwiftUICharts/PieChart/Models/{ => DataSets}/PieDataSet.swift (100%) create mode 100644 Sources/SwiftUICharts/PieChart/Views/MultiLayerPie.swift diff --git a/Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift b/Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift new file mode 100644 index 00000000..8e4e0f46 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift @@ -0,0 +1,37 @@ +// +// MultiPieDataPoint.swift +// +// +// Created by Will Dale on 22/02/2021. +// + +import SwiftUI + +public struct MultiPieDataPoint: CTMultiPieChartDataPoint { + + public var id: UUID = UUID() + // CTPieDataPoint + public var startAngle : Double = 0 + public var amount : Double = 0 + // CTChartDataPoint + public var value : Double + public var pointDescription : String? + public var date : Date? + // CTMultiPieChartDataPoint + public var layerDataPoints : [MultiPieDataPoint]? + + public var colour : Color + + public init(value : Double, + pointDescription: String? = nil, + date : Date? = nil, + colour : Color = Color.red, + layerDataPoints : [MultiPieDataPoint]? = nil + ) { + self.value = value + self.pointDescription = pointDescription + self.date = date + self.colour = colour + self.layerDataPoints = layerDataPoints + } +} diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartDataPoint.swift b/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift similarity index 100% rename from Sources/SwiftUICharts/PieChart/Models/PieChartDataPoint.swift rename to Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift diff --git a/Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift b/Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift new file mode 100644 index 00000000..c32a2413 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift @@ -0,0 +1,20 @@ +// +// MultiPieDataSet.swift +// +// +// Created by Will Dale on 22/02/2021. +// + +import SwiftUI + +public struct MultiPieDataSet: SingleDataSet { + + public var id: UUID = UUID() + public var dataPoints : [MultiPieDataPoint] + + public init(dataPoints: [MultiPieDataPoint]) { + self.dataPoints = dataPoints + } + + public typealias DataPoint = MultiPieDataPoint +} diff --git a/Sources/SwiftUICharts/PieChart/Models/PieDataSet.swift b/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift similarity index 100% rename from Sources/SwiftUICharts/PieChart/Models/PieDataSet.swift rename to Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift diff --git a/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift index d0602776..86cd4896 100644 --- a/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift @@ -60,87 +60,3 @@ public final class MultiLayerPieChartData: MultiPieChartDataProtocol { public typealias DataPoint = MultiPieDataPoint public typealias CTStyle = PieChartStyle } - - - -// MARK: - Data Sets -public struct MultiPieDataSet: SingleDataSet { - - public var id: UUID = UUID() - public var dataPoints : [MultiPieDataPoint] - - public init(dataPoints: [MultiPieDataPoint]) { - self.dataPoints = dataPoints - } - - public typealias DataPoint = MultiPieDataPoint - -} - - - -// MARK: - Data Point -public struct MultiPieDataPoint: CTPieDataPoint { - - public var id: UUID = UUID() - // CTPieDataPoint - public var startAngle : Double = 0 - public var amount : Double = 0 - // CTChartDataPoint - public var value : Double - public var pointDescription : String? - public var date : Date? - - public var colour : Color - - public var layerDataPoints : [MultiPieDataPoint]? - - public init(value : Double, - pointDescription: String? = nil, - date : Date? = nil, - colour : Color = Color.red, - layerDataPoints : [MultiPieDataPoint]? = nil - ) { - self.value = value - self.pointDescription = pointDescription - self.date = date - self.colour = colour - - self.layerDataPoints = layerDataPoints - - } - -} - -// MARK: - View - -public struct MultiLayerPie: View where ChartData: MultiLayerPieChartData { - - @ObservedObject var chartData: ChartData - - public init(chartData: ChartData) { - self.chartData = chartData - } - - public var body: some View { - - ZStack { - ForEach(chartData.dataSets.dataPoints, id: \.self) { data in - PieSegmentShape(id: data.id, - startAngle: data.startAngle, - amount: data.amount) - .fill(data.colour) - - if let points = data.layerDataPoints { - ForEach(points, id: \.self) { point in - DoughnutSegmentShape(id: point.id, - startAngle: point.startAngle, - amount: point.amount) - .strokeBorder(point.colour, lineWidth: 60) - } - - } - } - } - } -} diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift index 1249b34e..daaaa9a8 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift @@ -53,31 +53,56 @@ extension PieAndDoughnutChartDataProtocol where Set == MultiPieDataSet, DataPoin self.dataSets.dataPoints.indices.forEach { (point) in let amount = .pi * 2 * (self.dataSets.dataPoints[point].value / total) - self.dataSets.dataPoints[point].startAngle = startAngle self.dataSets.dataPoints[point].amount = amount let layerTotal = self.dataSets.dataPoints[point].layerDataPoints?.reduce(0) { $0 + $1.value } ?? 0 var layerStartAngle = startAngle - self.dataSets.dataPoints[point].layerDataPoints?.indices.forEach { (layer) in - let layerValue = self.dataSets.dataPoints[point].layerDataPoints?[layer].value ?? 0 let layerAmount = amount * (layerValue / layerTotal) - self.dataSets.dataPoints[point].layerDataPoints?[layer].startAngle = layerStartAngle self.dataSets.dataPoints[point].layerDataPoints?[layer].amount = layerAmount + + let layerTwoTotal = self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?.reduce(0) { $0 + $1.value } ?? 0 + var layerTwoStartAngle = layerStartAngle + self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?.indices.forEach { (layerTwo) in + let layerTwoValue = self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].value ?? 0 + let layerTwoAmount = layerAmount * (layerTwoValue / layerTwoTotal) + self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].startAngle = layerTwoStartAngle + self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].amount = layerTwoAmount + + + + let layerThreeTotal = self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].layerDataPoints?.reduce(0) { $0 + $1.value } ?? 0 + var layerThreeStartAngle = layerTwoStartAngle + self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].layerDataPoints?.indices.forEach { (layerThree) in + let layerThreeValue = self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].layerDataPoints?[layerThree].value ?? 0 + let layerThreeAmount = layerTwoAmount * (layerThreeValue / layerThreeTotal) + self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].layerDataPoints?[layerThree].startAngle = layerThreeStartAngle + self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].layerDataPoints?[layerThree].amount = layerThreeAmount + + layerThreeStartAngle += layerThreeAmount + } + + + + layerTwoStartAngle += layerTwoAmount + } + + layerStartAngle += layerAmount - } + startAngle += amount } } } + // * (180 / Double.pi) extension PieAndDoughnutChartDataProtocol where Set == PieDataSet, DataPoint == PieChartDataPoint { @@ -154,7 +179,9 @@ public protocol CTPieDataPoint: CTChartDataPoint { var amount : Double { get set } } -public protocol CTMultiPieChartDataPoints: CTChartDataPoint {} +public protocol CTMultiPieChartDataPoint: CTChartDataPoint { + var layerDataPoints : [MultiPieDataPoint]? { get set } +} diff --git a/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift b/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift index 72c46f07..085de5f3 100644 --- a/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift +++ b/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift @@ -13,6 +13,8 @@ internal struct PieSegmentShape: Shape, Identifiable { var startAngle : Double var amount : Double + + internal func path(in rect: CGRect) -> Path { let radius = min(rect.width, rect.height) / 2 diff --git a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift index a174a210..0baba021 100644 --- a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift @@ -23,7 +23,7 @@ public struct DoughnutChart: View where ChartData: DoughnutChartData DoughnutSegmentShape(id: chartData.dataSets.dataPoints[data].id, startAngle: chartData.dataSets.dataPoints[data].startAngle, amount: chartData.dataSets.dataPoints[data].amount) - .strokeBorder(chartData.dataSets.dataPoints[data].colour, lineWidth: chartData.chartStyle.strokeWidth) + .stroke/*Border*/(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)) diff --git a/Sources/SwiftUICharts/PieChart/Views/MultiLayerPie.swift b/Sources/SwiftUICharts/PieChart/Views/MultiLayerPie.swift new file mode 100644 index 00000000..8474ca2d --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Views/MultiLayerPie.swift @@ -0,0 +1,61 @@ +// +// MultiLayerPie.swift +// +// +// Created by Will Dale on 22/02/2021. +// + +import SwiftUI + +public struct MultiLayerPie: View where ChartData: MultiLayerPieChartData { + + @ObservedObject var chartData: ChartData + + public init(chartData: ChartData) { + self.chartData = chartData + } + + @State private var incept: CGFloat = 0 + + public var body: some View { + + ZStack { + ForEach(chartData.dataSets.dataPoints, id: \.self) { data in + PieSegmentShape(id: data.id, + startAngle: data.startAngle, + amount: data.amount) + .fill(data.colour) + + if let points = data.layerDataPoints { + ForEach(points, id: \.self) { point in + DoughnutSegmentShape(id: point.id, + startAngle: point.startAngle, + amount: point.amount) + .strokeBorder(point.colour, lineWidth: 120) + + + + if let pointsTwo = point.layerDataPoints { + ForEach(pointsTwo, id: \.self) { pointTwo in + DoughnutSegmentShape(id: pointTwo.id, + startAngle: pointTwo.startAngle, + amount: pointTwo.amount) + .strokeBorder(pointTwo.colour, lineWidth: 80) + + + if let pointsThree = pointTwo.layerDataPoints { + ForEach(pointsThree, id: \.self) { pointThree in + DoughnutSegmentShape(id: pointThree.id, + startAngle: pointThree.startAngle, + amount: pointThree.amount) + .strokeBorder(pointThree.colour, lineWidth: 40) + } + } + } + } + } + } + } + } + } +} From 3ee5f87703e7c5d9a5efe7808af56852aff0a01d Mon Sep 17 00:00:00 2001 From: Will Dale Date: Tue, 23 Feb 2021 08:47:36 +0000 Subject: [PATCH 084/152] Add documentation to bar chart types. --- .../BarChart/Extras/BarChartEnums.swift | 4 - .../Models/ChartData/BarChartData.swift | 177 ++----------- .../ChartData/GroupedBarChartData.swift | 236 +++++------------- .../ChartData/StackedBarChartData.swift | 93 ++++++- .../{Types => Models}/CornerRadius.swift | 13 +- .../BarChart/Models/DataSet/BarDataSet.swift | 54 +--- .../Models/DataSet/MultiBarDataSets.swift | 47 +++- .../Models/Datapoints/BarChartDataPoint.swift | 62 +---- .../Datapoints/MultiBarChartDataPoint.swift | 48 ++++ ...hartDataPoint.swift => GroupingData.swift} | 59 +++-- .../Models/Protocols/BarChartProtocols.swift | 53 +--- .../BarChart/Models/Style/BarChartStyle.swift | 63 ++--- .../BarChart/Models/Style/BarStyle.swift | 41 +-- .../Shapes/RoundedRectangleBarShape.swift | 32 ++- .../BarChart/Views/BarChart.swift | 37 ++- .../BarChart/Views/GroupedBarChart.swift | 33 ++- .../BarChart/Views/StackedBarChart.swift | 95 +++---- .../Views/SubViews/BarChartSubViews.swift | 28 ++- .../BarChart/Views/SubViews/Bars.swift | 111 +++++++- .../Shared/Views/LegendView.swift | 2 +- 20 files changed, 585 insertions(+), 703 deletions(-) rename Sources/SwiftUICharts/BarChart/{Types => Models}/CornerRadius.swift (75%) create mode 100644 Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift rename Sources/SwiftUICharts/BarChart/Models/{Datapoints/GroupedBarChartDataPoint.swift => GroupingData.swift} (61%) diff --git a/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift b/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift index a9cb51d9..15762f18 100644 --- a/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift +++ b/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift @@ -13,8 +13,6 @@ import Foundation case barStyle // From BarStyle data model case dataPoints // From each data point ``` - - - Tag: ColourFrom */ public enum ColourFrom { case barStyle @@ -34,8 +32,6 @@ public enum ColourFrom { case topTrailing // From top and trailing edges meeting at touch location. ``` - - - Tag: BarMarkerType */ public enum BarMarkerType: MarkerType { /// No overlay markers. diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 2aec1c2a..301f5e21 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -8,165 +8,38 @@ import SwiftUI /** - Data for drawing and styling a bar chart. - - This model contains all the data and styling information for a single data set bar chart. - + Data for drawing and styling a standard Bar Chart. + # Example ``` static func weekOfData() -> BarChartData { let data : BarDataSet = 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") + BarChartDataPoint(value: 20, xAxisLabel: "M", pointLabel: "Monday" , colour: .purple), + BarChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday" , colour: .blue), + BarChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday", colour: Color(.cyan)), + BarChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday" , colour: .green), + BarChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday" , colour: .yellow), + BarChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday" , colour: .orange), + BarChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday" , colour: .red) ], - legendTitle: "Data", - style: BarStyle()) - - let metadata : ChartMetadata = ChartMetadata(title : "Test Data", - subtitle : "A weeks worth") - - let labels : [String] = ["Mon", "Thu", "Sun"] - - let chartStyle : BarChartStyle = BarChartStyle(infoBoxPlacement: .floating, - xAxisGridStyle : GridStyle(), - yAxisGridStyle : GridStyle(), - xAxisLabelPosition: .bottom, - xAxisLabelsFrom: .dataPoint, - yAxisLabelPosition: .leading, - yAxisNumberOfLabels: 5) - - return BarChartData(dataSets : data, - metadata : metadata, - xAxisLabels : labels, - chartStyle : chartStyle, - noDataText : Text("No Data"), - calculations: .none) + legendTitle: "Data") + + return BarChartData(dataSets : data, + metadata : ChartMetadata(title : "Test Data", + subtitle: "A weeks worth"), + barStyle : BarStyle(barWidth : 0.5, + colourFrom: .dataPoints, + colour : .blue), + chartStyle: BarChartStyle(infoBoxPlacement : .floating, + xAxisLabelPosition : .bottom, + xAxisLabelsFrom : .dataPoint, + yAxisLabelPosition : .leading, + yAxisNumberOfLabels: 5)) } - - ``` - - --- - - # Parts - ## BarChartDataPoint - ### Options - Common to all. - ``` - BarChartDataPoint(value: Double, - xAxisLabel: String?, - pointLabel: String?, - date: Date?, - ...) - ``` - - Single Colour. - ``` - BarChartDataPoint(... - colour: Color?) - ``` - - Gradient Colours. - ``` - BarChartDataPoint(... - colours: [Color]?, - startPoint: UnitPoint?, - endPoint: UnitPoint?) - ``` - - Gradient Colours with stop control. - ``` - BarChartDataPoint(... - stops: [GradientStop]?, - startPoint: UnitPoint?, - endPoint: UnitPoint?) - ``` - ## BarStyle - ### Options ``` - BarStyle(barWidth : CGFloat, - cornerRadius : CornerRadius, - colourFrom : ColourFrom, - ...) - - BarStyle(... - colour: Color) - - BarStyle(... - colours: [Color], - startPoint: UnitPoint, - endPoint: UnitPoint) - - BarStyle(... - stops: [GradientStop], - startPoint: UnitPoint, - endPoint: UnitPoint) - ``` - - ## ChartMetadata - ``` - ChartMetadata(title: String?, subtitle: String?) - ``` - - ## BarChartStyle - ``` - BarChartStyle(infoBoxPlacement : InfoBoxPlacement, - infoBoxValueColour : Color, - infoBoxDescriptionColor : Color, - xAxisGridStyle : GridStyle, - xAxisLabelPosition : XAxisLabelPosistion, - xAxisLabelColour : Color, - xAxisLabelsFrom : LabelsFrom, - yAxisGridStyle : GridStyle, - yAxisLabelPosition : YAxisLabelPosistion, - yAxisLabelColour : Color, - yAxisNumberOfLabels : Int, - globalAnimation : Animation) - ``` - - ### GridStyle - ``` - GridStyle(numberOfLines: Int, - lineColour : Color, - lineWidth : CGFloat, - dash : [CGFloat], - dashPhase : CGFloat) - ``` - - --- - - # Also See - - [BarDataSet](x-source-tag://BarDataSet) - - [BarChartDataPoint](x-source-tag://BarChartDataPoint) - - [BarStyle](x-source-tag://BarStyle) - - [ColourType](x-source-tag://ColourType) - - [CornerRadius](x-source-tag://CornerRadius) - - [ColourFrom](x-source-tag://ColourFrom) - - [GradientStop](x-source-tag://GradientStop) - - [Chart Metadata](x-source-tag://ChartMetadata) - - [BarChartStyle](x-source-tag://BarChartStyle) - - [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) - - [GridStyle](x-source-tag://GridStyle) - - [XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) - - [LabelsFrom](x-source-tag://LabelsFrom) - - [YAxisLabelPosistion](x-source-tag://YAxisLabelPosistion) - - # Conforms to - - ObservableObject - - Identifiable - - BarChartDataProtocol - - LineAndBarChartData - - ChartData - - - Tag: BarChartData */ - public final class BarChartData: BarChartDataProtocol, LegendProtocol { // MARK: - Properties public let id : UUID = UUID() @@ -183,13 +56,14 @@ public final class BarChartData: BarChartDataProtocol, LegendProtocol { public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) - // MARK: - Initializers - /// Initialises a standard Bar Chart with optional calculation + // 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, @@ -205,6 +79,7 @@ public final class BarChartData: BarChartDataProtocol, LegendProtocol { self.barStyle = barStyle self.chartStyle = chartStyle self.noDataText = noDataText + self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (.bar, .single) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index b0d16322..9402ba13 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -8,186 +8,87 @@ 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. + Data model for drawing and styling a Grouped Bar Chart. + + The grouping data informs the model as to how the datapoints are linked. # Example ``` static func makeData() -> GroupedBarChartData { + + 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 data = MultiBarDataSet(dataSets: [ - BarDataSet(dataPoints: [ - BarChartDataPoint(value: 10, xAxisLabel: "1.1", pointLabel: "One One" , colour: .blue), - BarChartDataPoint(value: 20, xAxisLabel: "1.2", pointLabel: "One Two" , colour: .yellow), - BarChartDataPoint(value: 30, xAxisLabel: "1.3", pointLabel: "One Three", colour: .purple), - BarChartDataPoint(value: 40, xAxisLabel: "1.4", pointLabel: "One Four" , colour: .green)], - legendTitle: "One", - style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)), - BarDataSet(dataPoints: [ - BarChartDataPoint(value: 50, xAxisLabel: "2.1", pointLabel: "Two One" , colour: .blue), - BarChartDataPoint(value: 10, xAxisLabel: "2.2", pointLabel: "Two Two" , colour: .yellow), - BarChartDataPoint(value: 40, xAxisLabel: "2.3", pointLabel: "Two Three", colour: .purple), - BarChartDataPoint(value: 60, xAxisLabel: "2.3", pointLabel: "Two Three", colour: .green)], - legendTitle: "Two", - style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)), - BarDataSet(dataPoints: [ - BarChartDataPoint(value: 10, xAxisLabel: "3.1", pointLabel: "Three One" , colour: .blue), - BarChartDataPoint(value: 50, xAxisLabel: "3.2", pointLabel: "Three Two" , colour: .yellow), - BarChartDataPoint(value: 30, xAxisLabel: "3.3", pointLabel: "Three Three", colour: .purple), - BarChartDataPoint(value: 99, xAxisLabel: "3.4", pointLabel: "Three Four" , colour: .green)], - legendTitle: "Three", - style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)), - BarDataSet(dataPoints: [ - BarChartDataPoint(value: 80, xAxisLabel: "4.1", pointLabel: "Four One" , colour: .blue), - BarChartDataPoint(value: 10, xAxisLabel: "4.2", pointLabel: "Four Two" , colour: .yellow), - BarChartDataPoint(value: 20, xAxisLabel: "4.3", pointLabel: "Four Three", colour: .purple), - BarChartDataPoint(value: 50, xAxisLabel: "4.3", pointLabel: "Four Three", colour: .green)], - legendTitle: "Four", - style: BarStyle(barWidth: 1.0, colourFrom: .dataPoints)) + 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: 50, 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: 20, xAxisLabel: "2.1", pointLabel: "Two One" , group: Group.one.data), + MultiBarChartDataPoint(value: 60, 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: 30, xAxisLabel: "3.1", pointLabel: "Three One" , group: Group.one.data), + MultiBarChartDataPoint(value: 70, 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: 90, xAxisLabel: "3.4", pointLabel: "Three Four" , group: Group.four.data) + ]), + + MultiBarDataSet(dataPoints: [ + MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", pointLabel: "Four One" , group: Group.one.data), + MultiBarChartDataPoint(value: 80, 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 GroupedBarChartData(dataSets : data, - metadata : ChartMetadata(title: "Hello", subtitle: "Bob"), - xAxisLabels : ["Hello"], - chartStyle : BarChartStyle(), - noDataText : Text("No Data")) + groups : groups, + metadata : ChartMetadata(title: "Hello", subtitle: "Bob"), + chartStyle : BarChartStyle(infoBoxPlacement: .floating, + xAxisLabelsFrom : .dataPoint)) } ``` - - --- - - # Parts - # BarDataSet - ``` - BarDataSet(dataPoints: [BarChartDataPoint], - legendTitle: String, - style: BarStyle) - ``` - ## BarChartDataPoint - ### Options - Common to all. - ``` - BarChartDataPoint(value: Double, - xAxisLabel: String?, - pointLabel: String?, - date: Date?, - ...) - ``` - - Single Colour. - ``` - BarChartDataPoint(... - colour: Color?) - ``` - - Gradient Colours. - ``` - BarChartDataPoint(... - colours: [Color]?, - startPoint: UnitPoint?, - endPoint: UnitPoint?) - ``` - - Gradient Colours with stop control. - ``` - BarChartDataPoint(... - stops: [GradientStop]?, - startPoint: UnitPoint?, - endPoint: UnitPoint?) - ``` - ## BarStyle - ### Options - ``` - BarStyle(barWidth : CGFloat, - cornerRadius : CornerRadius, - colourFrom : ColourFrom, - ...) - - BarStyle(... - colour: Color) - - BarStyle(... - colours: [Color], - startPoint: UnitPoint, - endPoint: UnitPoint) - - BarStyle(... - stops: [GradientStop], - startPoint: UnitPoint, - endPoint: UnitPoint) - ``` - - ## ChartMetadata - ``` - ChartMetadata(title: String?, subtitle: String?) - ``` - - ## BarChartStyle - ``` - BarChartStyle(infoBoxPlacement : InfoBoxPlacement, - infoBoxValueColour : Color, - infoBoxDescriptionColor : Color, - xAxisGridStyle : GridStyle, - xAxisLabelPosition : XAxisLabelPosistion, - xAxisLabelColour : Color, - xAxisLabelsFrom : LabelsFrom, - yAxisGridStyle : GridStyle, - yAxisLabelPosition : YAxisLabelPosistion, - yAxisLabelColour : Color, - yAxisNumberOfLabels : Int, - globalAnimation : Animation) - ``` - - ### GridStyle - ``` - GridStyle(numberOfLines: Int, - lineColour : Color, - lineWidth : CGFloat, - dash : [CGFloat], - dashPhase : CGFloat) - ``` - - --- - - # Also See - - [BarDataSet](x-source-tag://BarDataSet) - - [BarChartDataPoint](x-source-tag://BarChartDataPoint) - - [BarStyle](x-source-tag://BarStyle) - - [ColourType](x-source-tag://ColourType) - - [CornerRadius](x-source-tag://CornerRadius) - - [ColourFrom](x-source-tag://ColourFrom) - - [GradientStop](x-source-tag://GradientStop) - - [Chart Metadata](x-source-tag://ChartMetadata) - - [BarChartStyle](x-source-tag://BarChartStyle) - - [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) - - [GridStyle](x-source-tag://GridStyle) - - [XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) - - [LabelsFrom](x-source-tag://LabelsFrom) - - [YAxisLabelPosistion](x-source-tag://YAxisLabelPosistion) - - # Conforms to - - ObservableObject - - Identifiable - - BarChartDataProtocol - - LineAndBarChartData - - ChartData - - - Tag: GroupedBarChartData */ -public final class GroupedBarChartData: GroupedBarChartDataProtocol, LegendProtocol { +public final class GroupedBarChartData: MultiBarChartDataProtocol, LegendProtocol { // MARK: - Properties public let id : UUID = UUID() - @Published public var dataSets : GroupedBarDataSets + @Published public var dataSets : MultiBarDataSets @Published public var metadata : ChartMetadata @Published public var xAxisLabels : [String]? @Published public var barStyle : BarStyle @Published public var chartStyle : BarChartStyle @Published public var legends : [LegendData] @Published public var viewData : ChartViewData - @Published public var infoView : InfoViewData = InfoViewData() + @Published public var infoView : InfoViewData = InfoViewData() @Published public var groups : [GroupingData] public var noDataText : Text @@ -195,17 +96,18 @@ public final class GroupedBarChartData: GroupedBarChartDataProtocol, LegendProto var groupSpacing : CGFloat = 0 - // MARK: - Initializers - /// Initialises a multi part Bar Chart with optional calculation + // MARK: - Initializer + /// Initialises a Grouped Bar Chart. /// /// - Parameters: /// - dataSets: Data to draw and style the bars. - /// - groups: Data for how to group data points. + /// - 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 : GroupedBarDataSets, + public init(dataSets : MultiBarDataSets, groups : [GroupingData], metadata : ChartMetadata = ChartMetadata(), xAxisLabels : [String]? = nil, @@ -271,9 +173,9 @@ public final class GroupedBarChartData: GroupedBarChartDataProtocol, LegendProto } // MARK: - Touch - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [GroupedBarChartDataPoint] { + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [MultiBarChartDataPoint] { - var points : [GroupedBarChartDataPoint] = [] + var points : [MultiBarChartDataPoint] = [] // Divide the chart into equal sections. let superXSection : CGFloat = (chartSize.size.width / CGFloat(dataSets.dataSets.count)) @@ -407,7 +309,7 @@ public final class GroupedBarChartData: GroupedBarChartDataProtocol, LegendProto return legends.sorted { $0.prioity < $1.prioity} } - public typealias Set = GroupedBarDataSets - public typealias DataPoint = GroupedBarChartDataPoint + public typealias Set = MultiBarDataSets + public typealias DataPoint = MultiBarChartDataPoint public typealias CTStyle = BarChartStyle } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index e11616b5..b7cbd58b 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -7,25 +7,101 @@ import SwiftUI -public final class StackedBarChartData: GroupedBarChartDataProtocol, LegendProtocol { +/** + 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: MultiBarChartDataProtocol, LegendProtocol { // MARK: - Properties public let id : UUID = UUID() - @Published public var dataSets : GroupedBarDataSets + @Published public var dataSets : MultiBarDataSets @Published public var metadata : ChartMetadata @Published public var xAxisLabels : [String]? @Published public var barStyle : BarStyle @Published public var chartStyle : BarChartStyle @Published public var legends : [LegendData] @Published public var viewData : ChartViewData - @Published public var infoView : InfoViewData = InfoViewData() + @Published public var infoView : InfoViewData = InfoViewData() @Published public var groups : [GroupingData] public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) - public init(dataSets : GroupedBarDataSets, + // 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, @@ -83,9 +159,9 @@ public final class StackedBarChartData: GroupedBarChartDataProtocol, LegendProto } // MARK: - Touch - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [GroupedBarChartDataPoint] { + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [MultiBarChartDataPoint] { - var points : [GroupedBarChartDataPoint] = [] + var points : [MultiBarChartDataPoint] = [] // Filter to get the right dataset based on the x axis. let superXSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataSets.count) @@ -252,8 +328,7 @@ public final class StackedBarChartData: GroupedBarChartDataProtocol, LegendProto return legends.sorted { $0.prioity < $1.prioity} } - - public typealias Set = GroupedBarDataSets - public typealias DataPoint = GroupedBarChartDataPoint + public typealias Set = MultiBarDataSets + public typealias DataPoint = MultiBarChartDataPoint public typealias CTStyle = BarChartStyle } diff --git a/Sources/SwiftUICharts/BarChart/Types/CornerRadius.swift b/Sources/SwiftUICharts/BarChart/Models/CornerRadius.swift similarity index 75% rename from Sources/SwiftUICharts/BarChart/Types/CornerRadius.swift rename to Sources/SwiftUICharts/BarChart/Models/CornerRadius.swift index 6e25f959..ffb080dd 100644 --- a/Sources/SwiftUICharts/BarChart/Types/CornerRadius.swift +++ b/Sources/SwiftUICharts/BarChart/Models/CornerRadius.swift @@ -9,18 +9,11 @@ import SwiftUI /** Corner radius of the bar shape. - - --- - - # Conforms to - Hashable - - - Tag: CornerRadius */ public struct CornerRadius: Hashable { - - var top : CGFloat - var bottom : CGFloat + + 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) { diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift index a1bedb6f..708e04e2 100644 --- a/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift +++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift @@ -8,7 +8,7 @@ import SwiftUI /** - Data set for a standard bar chart. + Data set for a bar chart. # Example ``` @@ -21,68 +21,22 @@ import SwiftUI BarChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), BarChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") ], - legendTitle: "Data", - style : BarStyle()) + legendTitle: "Data") ``` - - # BarChartDataPoint - ``` - BarChartDataPoint(value : Double, - xAxisLabel : String?, - pointLabel : String?, - date : Date?) - ``` - - # BarStyle - ``` - BarStyle(barWidth : CGFloat, - cornerRadius : CornerRadius, - colourFrom : ColourFrom, - ...) - - BarStyle(... - colour: Color) - - BarStyle(... - colours: [Color], - startPoint: UnitPoint, - endPoint: UnitPoint) - - BarStyle(... - stops: [GradientStop], - startPoint: UnitPoint, - endPoint: UnitPoint) - ``` - --- - # Also See - - [BarChartDataPoint](x-source-tag://BarChartDataPoint) - - [BarStyle](x-source-tag://BarStyle) - - [CornerRadius](x-source-tag://CornerRadius) - - [ColourFrom](x-source-tag://ColourFrom) - - [GradientStop](x-source-tag://GradientStop) - - # Conforms to - - SingleDataSet - - DataSet - - Hashable - - Identifiable - - - Tag: BarDataSet */ public struct BarDataSet: CTStandardBarChartDataSet { - public let id : UUID + public let id : UUID = UUID() public var dataPoints : [BarChartDataPoint] public var legendTitle : String - /// Initialises a new data set for a Bar Chart. + /// Initialises a new data set for a stabdard Bar Chart. /// - Parameters: /// - dataPoints: Array of elements. /// - legendTitle: label for the data in legend. public init(dataPoints : [BarChartDataPoint], legendTitle : String ) { - self.id = UUID() self.dataPoints = dataPoints self.legendTitle = legendTitle } diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift index 84e62bfd..98cb3b57 100644 --- a/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift +++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift @@ -7,31 +7,54 @@ import SwiftUI -public struct GroupedBarDataSets: MultiDataSet { +/** + Main data set for a multi part bar charts. + + # Example + ``` + let data = MultiBarDataSets(dataSets: [ + MultiBarDataSet(dataPoints: [ + MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", pointLabel: "One One", group: GroupingData(title: "One", colour: .blue)) + ]), + MultiBarDataSet(dataPoints: [ + MultiBarChartDataPoint(value: 20, xAxisLabel: "2.1", pointLabel: "Two One", group: GroupingData(title: "One", colour: .blue)) + ]) + ]) + ``` + */ +public struct MultiBarDataSets: MultiDataSet { - public let id : UUID - public var dataSets : [GroupedBarDataSet] + public let id : UUID = UUID() + public var dataSets : [MultiBarDataSet] /// Initialises a new data set for Multiline Line Chart. - public init(dataSets: [GroupedBarDataSet]) { - self.id = UUID() + public init(dataSets: [MultiBarDataSet]) { self.dataSets = dataSets } } +/** + Individual data sets for multi part bars charts. + + # Example + ``` + MultiBarDataSet(dataPoints: [ + MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", pointLabel: "One One", group: GroupingData(title: "One", colour: .blue)), + MultiBarChartDataPoint(value: 50, xAxisLabel: "1.2", pointLabel: "One Two", group: GroupingData(title: "Two", colour: .red)) + ]) + ``` + */ +public struct MultiBarDataSet: CTMultiBarChartDataSet { -public struct GroupedBarDataSet: CTGroupedBarChartDataSet { - - public let id : UUID - public var dataPoints : [GroupedBarChartDataPoint] + public let id : UUID = UUID() + public var dataPoints : [MultiBarChartDataPoint] /// Initialises a new data set for a Bar Chart. - public init(dataPoints : [GroupedBarChartDataPoint]) { - self.id = UUID() + public init(dataPoints : [MultiBarChartDataPoint]) { self.dataPoints = dataPoints } public typealias ID = UUID - public typealias DataPoint = GroupedBarChartDataPoint + public typealias DataPoint = MultiBarChartDataPoint public typealias Styling = BarStyle } diff --git a/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift index 141f8bf2..31bb721e 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift @@ -8,59 +8,17 @@ import SwiftUI /** - Data for a single data point. + Data for a single bar chart data point. - # Example - ``` - BarChartDataPoint(value: 20, - xAxisLabel: "M", - pointLabel: "Monday", - date: Date()) - ``` - - # Options - Common to all. - ``` - BarChartDataPoint(value: Double, - xAxisLabel: String?, - pointLabel: String?, - date: Date?, - ...) - ``` - - Single Colour. - ``` - BarChartDataPoint(... - colour: Color?) - ``` - - Gradient Colours. - ``` - BarChartDataPoint(... - colours: [Color]?, - startPoint: UnitPoint?, - endPoint: UnitPoint?) - ``` + Colour can be solid or gradient. - Gradient Colours with stop control. + # Example ``` - BarChartDataPoint(... - stops: [GradientStop]?, - startPoint: UnitPoint?, - endPoint: UnitPoint?) + BarChartDataPoint(value: 90, + xAxisLabel: "T", + pointLabel: "Tuesday", + colour: .blue) ``` - - # Also See - - [GradientStopt](x-source-tag://GradientStop) - - # Conforms to - - CTLineAndBarDataPoint - - CTChartDataPoint - - Hashable - - Identifiable - - CTColourStyle - - - Tag: BarChartDataPoint */ public struct BarChartDataPoint: CTStandardBarDataPoint { @@ -78,7 +36,7 @@ public struct BarChartDataPoint: CTStandardBarDataPoint { public var startPoint : UnitPoint? public var endPoint : UnitPoint? - // MARK: - init: single colour + // 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 @@ -104,7 +62,7 @@ public struct BarChartDataPoint: CTStandardBarDataPoint { self.colourType = .colour } - // MARK: - init: gradient colour + // MARK: - Gradient colour /// Data model for a single data point with colour for use with a bar chart. /// - Parameters: /// - value: Value of the data point @@ -136,7 +94,7 @@ public struct BarChartDataPoint: CTStandardBarDataPoint { self.colourType = .gradientColour } - // MARK: - init: gradient with stops + // MARK: - 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 diff --git a/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift new file mode 100644 index 00000000..a51ac1b6 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift @@ -0,0 +1,48 @@ +// +// 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", + pointLabel: "One One", + group: GroupingData(title: "One", colour: .blue)) + ``` + */ +public struct MultiBarChartDataPoint: CTMultiBarDataPoint { + + public let id = UUID() + + public var value : Double + public var xAxisLabel : String? + public var pointDescription : String? + public var date : Date? + + public var group : GroupingData + + public init(value : Double, + xAxisLabel : String? = nil, + pointLabel : String? = nil, + date : Date? = nil, + group: GroupingData + ) { + self.value = value + self.xAxisLabel = xAxisLabel + self.pointDescription = pointLabel + self.date = date + + self.group = group + + } + public typealias ID = UUID +} + diff --git a/Sources/SwiftUICharts/BarChart/Models/Datapoints/GroupedBarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/GroupingData.swift similarity index 61% rename from Sources/SwiftUICharts/BarChart/Models/Datapoints/GroupedBarChartDataPoint.swift rename to Sources/SwiftUICharts/BarChart/Models/GroupingData.swift index 9221d5d1..5c3e475d 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Datapoints/GroupedBarChartDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/GroupingData.swift @@ -1,40 +1,20 @@ // -// GroupedBarChartDataPoint.swift +// GroupingData.swift // // -// Created by Will Dale on 19/02/2021. +// Created by Will Dale on 23/02/2021. // import SwiftUI -public struct GroupedBarChartDataPoint: CTGroupedBarDataPoint { - - public let id = UUID() - - public var value : Double - public var xAxisLabel : String? - public var pointDescription : String? - public var date : Date? - - public var group : GroupingData - - public init(value : Double, - xAxisLabel : String? = nil, - pointLabel : String? = nil, - date : Date? = nil, - group: GroupingData - ) { - self.value = value - self.xAxisLabel = xAxisLabel - self.pointDescription = pointLabel - self.date = date - - self.group = group - - } - public typealias ID = UUID -} - +/** + Model for grouping data points together so they can be drawn in the correct groupings. + + # Example + ``` + GroupingData(title: "One", colour: .blue) + ``` + */ public struct GroupingData: CTColourStyle, Hashable, Identifiable { public let id : UUID = UUID() @@ -46,6 +26,11 @@ public struct GroupingData: CTColourStyle, Hashable, Identifiable { public var startPoint: UnitPoint? public var endPoint : UnitPoint? + // MARK: - Single colour + /// Group with single colour + /// - Parameters: + /// - title: Title for legends + /// - colour: Colour drawing the bars and legends public init(title : String, colour : Color ) { @@ -58,6 +43,13 @@ public struct GroupingData: CTColourStyle, Hashable, Identifiable { self.endPoint = nil } + // MARK: - Gradient colour + /// Group with gradient colours. + /// - Parameters: + /// - title: Title for legends + /// - colours: Colours drawing the bars and legends + /// - startPoint: Start point for Gradient. + /// - endPoint: End point for Gradient. public init(title : String, colours : [Color], startPoint : UnitPoint, @@ -72,6 +64,13 @@ public struct GroupingData: CTColourStyle, Hashable, Identifiable { self.endPoint = endPoint } + // MARK: - Gradient with stops + /// Group with gradient colours and stops control. + /// - Parameters: + /// - title: Title for legends + /// - stops: Colours drawing the bars and legends + /// - startPoint: Start point for Gradient. + /// - endPoint: End point for Gradient. public init(title : String, stops : [GradientStop], startPoint : UnitPoint, diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index c8337b0f..a88fe963 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -7,19 +7,9 @@ import SwiftUI - // MARK: - Chart Data /** A protocol to extend functionality of `LineAndBarChartData` specifically for Bar Charts. - - # Reference - - [See LineAndBarChartData](x-source-tag://LineAndBarChartData) - - [See LineAndBarChartData](x-source-tag://LineAndBarChartData) - - [See ChartData](x-source-tag://ChartData) - - `LineAndBarChartData` conforms to [ChartData](x-source-tag://ChartData) - - - Tag: BarChartDataProtocol */ public protocol BarChartDataProtocol: LineAndBarChartData { @@ -30,21 +20,9 @@ public protocol BarChartDataProtocol: LineAndBarChartData { } /** - A protocol to extend functionality of `LineAndBarChartData` specifically for Bar Charts. - - # Reference - - [See GroupedBarChartDataProtocol](x-source-tag://GroupedBarChartDataProtocol) - - [See BarChartDataProtocol](x-source-tag://BarChartDataProtocol) - - [See LineAndBarChartData](x-source-tag://LineAndBarChartData) - - [See ChartData](x-source-tag://ChartData) - - `GroupedBarChartDataProtocol` conforms to [BarChartDataProtocol](x-source-tag://ChartData) - - `LineAndBarChartData` conforms to [ChartData](x-source-tag://ChartData) - - - Tag: GroupedBarChartDataProtocol + A protocol to extend functionality of `BarChartDataProtocol` specifically for Multi Part Bar Charts. */ -public protocol GroupedBarChartDataProtocol: BarChartDataProtocol { +public protocol MultiBarChartDataProtocol: BarChartDataProtocol { /** Grouping data to inform the chart about the relationship between the datapoints. @@ -55,13 +33,10 @@ public protocol GroupedBarChartDataProtocol: BarChartDataProtocol { + // MARK: - Style /** A protocol to extend functionality of `CTLineAndBarChartStyle` specifically for Bar Charts. - - Currently empty. - - - Tag: CTBarChartStyle */ public protocol CTBarChartStyle: CTLineAndBarChartStyle {} @@ -71,14 +46,10 @@ public protocol CTBarChartStyle: CTLineAndBarChartStyle {} + // MARK: - DataSet /** A protocol to extend functionality of `SingleDataSet` specifically for Standard Bar Charts. - - # Reference - [See SingleDataSet](x-source-tag://SingleDataSet) - - - Tag: CTBarChartDataSet */ public protocol CTStandardBarChartDataSet: SingleDataSet { /** @@ -87,9 +58,12 @@ public protocol CTStandardBarChartDataSet: SingleDataSet { var legendTitle : String { get set } } -public protocol CTGroupedBarChartDataSet: SingleDataSet {} +/** + A protocol to extend functionality of `SingleDataSet` specifically for Multi Part Bar Charts. + */ +public protocol CTMultiBarChartDataSet: SingleDataSet {} + -public protocol CTSStackedBarChartDataSet: SingleDataSet {} @@ -101,24 +75,19 @@ public protocol CTSStackedBarChartDataSet: SingleDataSet {} // MARK: - DataPoints /** A protocol to extend functionality of `CTLineAndBarDataPoint` specifically for standard Bar Charts. - - - Tag: CTStandardBarDataPoint */ public protocol CTBarDataPoint: CTLineAndBarDataPoint {} /** A protocol to extend functionality of `CTLineAndBarDataPoint` specifically for standard Bar Charts. - - - Tag: CTStandardBarDataPoint */ public protocol CTStandardBarDataPoint: CTBarDataPoint, CTColourStyle {} + /** A protocol to extend functionality of `CTLineAndBarDataPoint` specifically for multi part Bar Charts. i.e: Grouped or Stacked - - - Tag: CTMultiPartBarDataPoint */ -public protocol CTGroupedBarDataPoint: CTBarDataPoint { +public protocol CTMultiBarDataPoint: CTBarDataPoint { var group : GroupingData { get set } diff --git a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift index 8ca0c754..e526ad93 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift @@ -7,6 +7,7 @@ import SwiftUI + /** Control of the overall aesthetic of the bar chart. @@ -14,59 +15,22 @@ import SwiftUI specific to the data set(s), # Example +``` + BarChartStyle(infoBoxPlacement : .floating, + markerType : .full, + xAxisLabelPosition : .bottom, + xAxisLabelsFrom : .dataPoint, + yAxisLabelPosition : .leading, + yAxisNumberOfLabels: 5, + globalAnimation : .linear(duration: 1)) ``` - BarChartStyle(infoBoxPlacement: .header, - xAxisGridStyle : GridStyle(numberOfLines: 7, - lineColour : .gray, - lineWidth : 1, - dash : [8], - dashPhase : 0), - yAxisGridStyle : GridStyle(numberOfLines: 7, - lineColour : .gray, - lineWidth : 1, - dash : [8], - dashPhase : 0), - xAxisLabelPosition : .bottom, - xAxisLabelsFrom : .dataPoint, - yAxisLabelPosition : .leading, - yAxisNumberOfLabels : 5, - baseline : .minimumValue, - globalAnimation : .linear(duration: 1)) - ``` - - # Options - ``` - BarChartStyle(infoBoxPlacement : InfoBoxPlacement, - xAxisGridStyle : GridStyle, - yAxisGridStyle : GridStyle, - xAxisLabelPosition : XAxisLabelPosistion, - xAxisLabelsFrom : LabelsFrom, - yAxisLabelPosition : YAxisLabelPosistion, - yAxisNumberOfLabels : Int, - globalAnimation : Animation) - ``` - - --- - - # Also See - - [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) - - [GridStyle](x-source-tag://GridStyle) - - [XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) - - [LabelsFrom](x-source-tag://LabelsFrom) - - [YAxisLabelPosistion](x-source-tag://YAxisLabelPosistion) - - # Conforms to - - CTBarChartStyle - - CTLineAndBarChartStyle - - CTChartStyle - - - Tag: BarChartStyle */ public struct BarChartStyle: CTBarChartStyle { public var infoBoxPlacement : InfoBoxPlacement public var infoBoxValueColour : Color public var infoBoxDescriptionColor : Color + public var markerType : BarMarkerType public var xAxisGridStyle : GridStyle @@ -81,12 +45,13 @@ public struct BarChartStyle: CTBarChartStyle { public var globalAnimation : Animation - /// Model for controlling the overall aesthetic of the chart. + /// 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. /// - infoBoxDescriptionColor: Colour of the description part 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. @@ -103,6 +68,7 @@ public struct BarChartStyle: CTBarChartStyle { public init(infoBoxPlacement : InfoBoxPlacement = .floating, infoBoxValueColour : Color = Color.primary, infoBoxDescriptionColor : Color = Color.primary, + markerType : BarMarkerType = .full, xAxisGridStyle : GridStyle = GridStyle(), @@ -120,6 +86,7 @@ public struct BarChartStyle: CTBarChartStyle { self.infoBoxPlacement = infoBoxPlacement self.infoBoxValueColour = infoBoxValueColour self.infoBoxDescriptionColor = infoBoxDescriptionColor + self.markerType = markerType self.xAxisGridStyle = xAxisGridStyle diff --git a/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift index a729a9a3..5d4ddffd 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift @@ -8,7 +8,7 @@ import SwiftUI /** - Model for controlling the aesthetic of the line chart. + Model for controlling the aesthetic of the bars. # Example ``` @@ -17,43 +17,6 @@ import SwiftUI colourFrom : .barStyle, colour : .blue) ``` - - --- - - # Options - ``` - BarStyle(barWidth : CGFloat, - cornerRadius : CornerRadius, - colourFrom : ColourFrom, - ...) - - BarStyle(... - colour: Color) - - BarStyle(... - colours: [Color], - startPoint: UnitPoint, - endPoint: UnitPoint) - - BarStyle(... - stops: [GradientStop], - startPoint: UnitPoint, - endPoint: UnitPoint) - ``` - - --- - - # Also See - - [ColourType](x-source-tag://ColourType) - - [CornerRadius](x-source-tag://CornerRadius) - - [ColourFrom](x-source-tag://ColourFrom) - - [GradientStop](x-source-tag://GradientStop) - - # Conforms to - - CTColourStyle - - Hashable - - - Tag: BarStyle */ public struct BarStyle: CTColourStyle, Hashable { @@ -128,7 +91,7 @@ public struct BarStyle: CTColourStyle, Hashable { /// - stops: Colours and Stops for Gradient with stop control. /// - startPoint: Start point for Gradient. /// - endPoint: End point for Gradient. - public init(barWidth : CGFloat = 1, + 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)], diff --git a/Sources/SwiftUICharts/BarChart/Shapes/RoundedRectangleBarShape.swift b/Sources/SwiftUICharts/BarChart/Shapes/RoundedRectangleBarShape.swift index e1195998..9c350e4e 100644 --- a/Sources/SwiftUICharts/BarChart/Shapes/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 + + [Reference](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 index f48509b5..123c9eed 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift @@ -7,10 +7,39 @@ 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) + .yAxisPOI(chartData: data) + .xAxisGrid(chartData: data) + .yAxisGrid(chartData: data) + .xAxisLabels(chartData: data) + .yAxisLabels(chartData: data) + .infoBox(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 } @@ -23,13 +52,13 @@ public struct BarChart: View where ChartData: BarChartData { switch chartData.barStyle.colourFrom { case .barStyle: - BarChartDataSetSubView(chartData : chartData, - dataPoint : dataPoint) + BarChartDataSetSubView(chartData: chartData, + dataPoint: dataPoint) case .dataPoints: - BarChartDataPointSubView(chartData : chartData, - dataPoint : dataPoint) + BarChartDataPointSubView(chartData: chartData, + dataPoint: dataPoint) } } diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index e08d0bee..b24acd52 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -7,12 +7,43 @@ 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) + .yAxisPOI(chartData: data) + .xAxisGrid(chartData: data) + .yAxisGrid(chartData: data) + .xAxisLabels(chartData: data) + .yAxisLabels(chartData: data) + .infoBox(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 diff --git a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift index 99fc8383..f8b5001e 100644 --- a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift @@ -7,10 +7,40 @@ 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) + .yAxisPOI(chartData: data) + .xAxisGrid(chartData: data) + .yAxisGrid(chartData: data) + .xAxisLabels(chartData: data) + .yAxisLabels(chartData: data) + .infoBox(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 } @@ -24,7 +54,7 @@ public struct StackedBarChart: View where ChartData: StackedBarChartD HStack(alignment: .bottom, spacing: 0) { ForEach(chartData.dataSets.dataSets) { dataSet in - MultiPartBarSubView(dataSet: dataSet) + StackElementSubView(dataSet: dataSet) .scaleEffect(y: startAnimation ? CGFloat(DataFunctions.dataSetMaxValue(from: dataSet) / chartData.getMaxValue()) : 0, anchor: .bottom) .scaleEffect(x: chartData.barStyle.barWidth, anchor: .center) .animateOnAppear(using: chartData.chartStyle.globalAnimation) { @@ -39,64 +69,3 @@ public struct StackedBarChart: View where ChartData: StackedBarChartD } else { CustomNoDataView(chartData: chartData) } } } - -/** - - */ -internal struct MultiPartBarSubView: View { - - private let dataSet : GroupedBarDataSet - - internal init(dataSet: GroupedBarDataSet) { - self.dataSet = dataSet - } - - internal var body: some View { - GeometryReader { geo in - - VStack(spacing: 0) { - ForEach(dataSet.dataPoints.reversed()) { dataPoint in - - if dataPoint.group.colourType == .colour, - let colour = dataPoint.group.colour - { - - ColourPartBar(colour, getHeight(height : geo.size.height, - dataSet : dataSet, - dataPoint : dataPoint)) - - } else if dataPoint.group.colourType == .gradientColour, - let colours = dataPoint.group.colours, - let startPoint = dataPoint.group.startPoint, - let endPoint = dataPoint.group.endPoint - { - - GradientColoursPartBar(colours, startPoint, endPoint, getHeight(height: geo.size.height, - dataSet : dataSet, - dataPoint : dataPoint)) - - } else if dataPoint.group.colourType == .gradientStops, - let stops = dataPoint.group.stops, - let startPoint = dataPoint.group.startPoint, - let endPoint = dataPoint.group.endPoint - { - - let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - - GradientStopsPartBar(safeStops, startPoint, endPoint, getHeight(height: geo.size.height, - dataSet : dataSet, - dataPoint : dataPoint)) - } - - } - } - } - } - - - private func getHeight(height: CGFloat, dataSet: GroupedBarDataSet, dataPoint: GroupedBarChartDataPoint) -> CGFloat { - let value = dataPoint.value - let sum = dataSet.dataPoints.reduce(0) { $0 + $1.value } - return height * CGFloat(value / sum) - } -} diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift index 9e310367..42cff425 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift @@ -7,16 +7,22 @@ import SwiftUI +// MARK: - Chart Data /** Bar segment where the colour information comes from chart style. */ internal struct BarChartDataSetSubView: View { + + private let chartData : CD + private let dataPoint : BarChartDataPoint - let chartData : CD - let dataPoint : BarChartDataPoint - - @State private var startAnimation : Bool = false - + internal init(chartData: CD, + dataPoint: BarChartDataPoint + ) { + self.chartData = chartData + self.dataPoint = dataPoint + } + internal var body: some View { if chartData.barStyle.colourType == .colour, let colour = chartData.barStyle.colour @@ -46,13 +52,21 @@ internal struct BarChartDataSetSubView: View { } } +// MARK: - DataPoints /** Bar segment where the colour information comes from datapoints. */ internal struct BarChartDataPointSubView: View { - let chartData : CD - let dataPoint : BarChartDataPoint + private let chartData : CD + private let dataPoint : BarChartDataPoint + + internal init(chartData: CD, + dataPoint: BarChartDataPoint + ) { + self.chartData = chartData + self.dataPoint = dataPoint + } internal var body: some View { diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift index 1e87598a..4de8474e 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift @@ -8,6 +8,11 @@ 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 colour : Color @@ -25,10 +30,10 @@ internal struct ColourBar: View { _ cornerRadius: CornerRadius, _ barWidth : CGFloat ) { - self.colour = colour - self.dataPoint = dataPoint - self.maxValue = maxValue - self.chartStyle = chartStyle + self.colour = colour + self.dataPoint = dataPoint + self.maxValue = maxValue + self.chartStyle = chartStyle self.cornerRadius = cornerRadius self.barWidth = barWidth } @@ -52,6 +57,11 @@ internal struct ColourBar: View { } } +/** + Sub view of a single bar using colour gradient. + + For Standard and Grouped Bar Charts. + */ internal struct GradientColoursBar: View { private let colours : [Color] @@ -104,6 +114,11 @@ internal struct GradientColoursBar: View { } } +/** + Sub view of a single bar using colour gradient with stop control. + + For Standard and Grouped Bar Charts. + */ internal struct GradientStopsBar: View { private let stops : [Gradient.Stop] @@ -159,7 +174,12 @@ internal struct GradientStopsBar: View { -// MARK: - Multi Part +// MARK: - Grouped +/** + Sub view of an element of a bar using a single colour. + + For Stacked Bar Charts. + */ internal struct ColourPartBar: View { private let colour : Color @@ -179,6 +199,11 @@ internal struct ColourPartBar: View { } } +/** + 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] @@ -206,6 +231,11 @@ internal struct GradientColoursPartBar: View { } } +/** + 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] @@ -232,3 +262,74 @@ internal struct GradientStopsPartBar: View { .frame(height: height) } } + +// MARK: - Stacked +/** + Individual elements that make up a single bar. + */ +internal struct StackElementSubView: View { + + private let dataSet : MultiBarDataSet + + internal init(dataSet: MultiBarDataSet) { + self.dataSet = dataSet + } + + internal var body: some View { + GeometryReader { geo in + + VStack(spacing: 0) { + ForEach(dataSet.dataPoints.reversed()) { dataPoint in + + if dataPoint.group.colourType == .colour, + let colour = dataPoint.group.colour + { + + ColourPartBar(colour, getHeight(height : geo.size.height, + dataSet : dataSet, + dataPoint : dataPoint)) + + } else if dataPoint.group.colourType == .gradientColour, + let colours = dataPoint.group.colours, + let startPoint = dataPoint.group.startPoint, + let endPoint = dataPoint.group.endPoint + { + + GradientColoursPartBar(colours, startPoint, endPoint, getHeight(height: geo.size.height, + dataSet : dataSet, + dataPoint : dataPoint)) + + } else if dataPoint.group.colourType == .gradientStops, + let stops = dataPoint.group.stops, + let startPoint = dataPoint.group.startPoint, + let endPoint = dataPoint.group.endPoint + { + + let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) + + GradientStopsPartBar(safeStops, startPoint, endPoint, getHeight(height: geo.size.height, + dataSet : dataSet, + dataPoint : dataPoint)) + } + + } + } + } + } + + /// 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) + } +} + diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index 0da672b1..d7d936de 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -56,7 +56,7 @@ internal struct LegendView: View where T: ChartData { return false } } else if chartData is GroupedBarChartData || chartData is StackedBarChartData { - if let datapoint = chartData.infoView.touchOverlayInfo.first as? GroupedBarChartDataPoint { + if let datapoint = chartData.infoView.touchOverlayInfo.first as? MultiBarChartDataPoint { return chartData.infoView.isTouchCurrent && legend.colour == datapoint.group.colour } else { return false From 3deb56e7fd33ca913d348fb5c74cd612fad912f8 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Tue, 23 Feb 2021 18:12:11 +0000 Subject: [PATCH 085/152] Update documentation. --- .../BarChart/Models/DataSet/BarDataSet.swift | 2 +- .../Datapoints/MultiBarChartDataPoint.swift | 2 +- .../BarChart/Models/Style/BarChartStyle.swift | 14 +- .../BarChart/Models/Style/BarStyle.swift | 4 +- .../BarChart/Views/BarChart.swift | 20 +- .../LineChart/Extras/LineChartEnums.swift | 33 +-- .../LineChart/Extras/PathExtensions.swift | 2 + .../Models/ChartData/LineChartData.swift | 151 ++------------ .../Models/ChartData/MultiLineChartData.swift | 188 +++--------------- .../Models/DataSet/LineDataSet.swift | 80 +------- .../Models/DataSet/MultiLineDataSet.swift | 130 +++--------- .../LineChart/Models/LineChartDataPoint.swift | 18 +- .../Models/Protocols/LineChartProtocols.swift | 43 ++-- .../LineChartProtocolsExtensions.swift | 28 ++- .../Models/Style/LineChartStyle.swift | 71 ++----- .../LineChart/Models/Style/LineStyle.swift | 61 ++---- .../LineChart/Models/Style/PointStyle.swift | 27 +-- .../LineChart/Shapes/LegendLine.swift | 2 +- .../LineChart/Shapes/LineShape.swift | 4 +- .../LineChart/Shapes/PointShape.swift | 43 ++-- .../ViewModifiers/PointMarkers.swift | 8 +- .../LineChart/Views/FilledLineChart.swift | 35 ++++ .../LineChart/Views/LineChartView.swift | 97 +++------ .../LineChart/Views/MultiLineChart.swift | 35 ++++ .../Views/SubViews/LineChartSubViews.swift | 30 ++- .../Views/SubViews/PointsSubView.swift | 3 + .../Models/ChartData/DoughnutChartData.swift | 79 ++++++++ .../ChartData/MultiLayerPieChartData.swift | 145 ++++++++++++++ .../{Pie => ChartData}/PieChartData.swift | 34 +++- .../Models/DataPoints/MultiPieDataPoint.swift | 25 ++- .../Models/DataPoints/PieChartDataPoint.swift | 16 ++ .../Models/DataSets/MultiPieDataSet.swift | 19 ++ .../PieChart/Models/DataSets/PieDataSet.swift | 24 ++- .../Models/Doughnut/DoughnutChartData.swift | 49 ----- .../MultiLayer/MultiLayerPieChartData.swift | 62 ------ .../PieChart/Models/PieSegmentStyle.swift | 32 --- .../PieChartProtocolExtentions.swift} | 131 +++--------- .../Models/Protocols/PieChartProtocols.swift | 98 +++++++++ .../DoughnutChartStyle.swift | 30 ++- .../Models/{Pie => Style}/PieChartStyle.swift | 27 ++- .../Shapes/DoughnutSegmentShape.swift | 40 ++++ .../PieChart/Shapes/PieSegmentShape.swift | 35 +--- .../PieChart/Views/DoughnutChart.swift | 23 +++ ...ayerPie.swift => MultiLayerPieChart.swift} | 25 ++- .../PieChart/Views/PieChart.swift | 23 +++ .../Shared/Extras/DataFunctions.swift | 65 +++++- .../Shared/Extras/Extensions.swift | 33 ++- .../Shared/Extras/SharedEnums.swift | 8 - .../Shared/Models/ChartMetadata.swift | 6 +- .../Shared/Models/InfoViewData.swift | 53 +++-- .../Shared/Models/LegendData.swift | 10 +- .../Models/Protocols/SharedProtocols.swift | 87 ++------ .../Shared/Types/GradientStop.swift | 8 +- .../Shared/Types/HashablePoint.swift | 2 + .../SwiftUICharts/Shared/Types/Stroke.swift | 65 +++--- .../Shared/ViewModifiers/HeaderBox.swift | 7 +- .../Shared/ViewModifiers/InfoBox.swift | 14 +- .../Shared/ViewModifiers/Legends.swift | 10 +- .../Shared/ViewModifiers/TouchOverlay.swift | 23 +-- .../Shared/Views/CustomNoDataView.swift | 3 + .../Shared/Views/LegendView.swift | 11 +- .../Shared/Views/PosistionIndicator.swift | 14 +- .../Shared/Views/TouchOverlayBox.swift | 3 + .../Protocols/LineAndBarProtocols.swift | 22 -- .../ViewModifiers/YAxisPOI.swift | 4 +- 65 files changed, 1223 insertions(+), 1273 deletions(-) create mode 100644 Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift create mode 100644 Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift rename Sources/SwiftUICharts/PieChart/Models/{Pie => ChartData}/PieChartData.swift (50%) delete mode 100644 Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift delete mode 100644 Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift delete mode 100644 Sources/SwiftUICharts/PieChart/Models/PieSegmentStyle.swift rename Sources/SwiftUICharts/PieChart/Models/{PieChartProtocols.swift => Protocols/PieChartProtocolExtentions.swift} (70%) create mode 100644 Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift rename Sources/SwiftUICharts/PieChart/Models/{Doughnut => Style}/DoughnutChartStyle.swift (54%) rename Sources/SwiftUICharts/PieChart/Models/{Pie => Style}/PieChartStyle.swift (51%) create mode 100644 Sources/SwiftUICharts/PieChart/Shapes/DoughnutSegmentShape.swift rename Sources/SwiftUICharts/PieChart/Views/{MultiLayerPie.swift => MultiLayerPieChart.swift} (78%) diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift index 708e04e2..ab85337c 100644 --- a/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift +++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift @@ -30,7 +30,7 @@ public struct BarDataSet: CTStandardBarChartDataSet { public var dataPoints : [BarChartDataPoint] public var legendTitle : String - /// Initialises a new data set for a stabdard Bar Chart. + /// Initialises a new data set for standard Bar Charts. /// - Parameters: /// - dataPoints: Array of elements. /// - legendTitle: label for the data in legend. diff --git a/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift index a51ac1b6..8d7f5932 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift @@ -20,7 +20,7 @@ import SwiftUI */ public struct MultiBarChartDataPoint: CTMultiBarDataPoint { - public let id = UUID() + public let id : UUID = UUID() public var value : Double public var xAxisLabel : String? diff --git a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift index e526ad93..3bc0ef5c 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift @@ -29,7 +29,7 @@ public struct BarChartStyle: CTBarChartStyle { public var infoBoxPlacement : InfoBoxPlacement public var infoBoxValueColour : Color - public var infoBoxDescriptionColor : Color + public var infoBoxDescriptionColour : Color public var markerType : BarMarkerType @@ -50,7 +50,7 @@ public struct BarChartStyle: CTBarChartStyle { /// - Parameters: /// - infoBoxPlacement: Placement of the information box that appears on touch input. /// - infoBoxValueColour: Colour of the value part of the touch info. - /// - infoBoxDescriptionColor: Colour of the description part of the touch info. + /// - infoBoxDescriptionColour: Colour of the description part of the touch info. /// /// - markerType: Where the marker lines come from to meet at a specified point. /// @@ -64,10 +64,10 @@ public struct BarChartStyle: CTBarChartStyle { /// - yAxisNumberOfLabel: Number Of Labels on Y Axis. /// - yAxisLabelColour: Text Colour for the labels on the Y axis. /// - /// - globalAnimation: Gobal control of animations. + /// - globalAnimation: Global control of animations. public init(infoBoxPlacement : InfoBoxPlacement = .floating, infoBoxValueColour : Color = Color.primary, - infoBoxDescriptionColor : Color = Color.primary, + infoBoxDescriptionColour: Color = Color.primary, markerType : BarMarkerType = .full, @@ -83,9 +83,9 @@ public struct BarChartStyle: CTBarChartStyle { globalAnimation : Animation = Animation.linear(duration: 1) ) { - self.infoBoxPlacement = infoBoxPlacement - self.infoBoxValueColour = infoBoxValueColour - self.infoBoxDescriptionColor = infoBoxDescriptionColor + self.infoBoxPlacement = infoBoxPlacement + self.infoBoxValueColour = infoBoxValueColour + self.infoBoxDescriptionColour = infoBoxDescriptionColour self.markerType = markerType diff --git a/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift index 5d4ddffd..07076f51 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift @@ -34,7 +34,7 @@ public struct BarStyle: CTColourStyle, Hashable { public var startPoint : UnitPoint? public var endPoint : UnitPoint? - + // MARK: - Single colour /// Bar Chart with single colour /// - Parameters: /// - barWidth: How much of the available width to use. 0...1 @@ -57,6 +57,7 @@ public struct BarStyle: CTColourStyle, Hashable { self.colourType = .colour } + // MARK: - Gradient colour /// Bar Chart with Gradient Colour /// - Parameters: /// - barWidth: How much of the available width to use. 0...1 @@ -83,6 +84,7 @@ public struct BarStyle: CTColourStyle, Hashable { self.colourType = .gradientColour } + // MARK: - Gradient with stops /// Bar Chart with Gradient with Stops /// - Parameters: /// - barWidth: How much of the available width to use. 0...1 diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift index 123c9eed..052bf904 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift @@ -22,16 +22,16 @@ import SwiftUI as the modifiers are various types for stacks that wrap around the previous views. ``` - .touchOverlay(chartData: data) - .averageLine(chartData: data) - .yAxisPOI(chartData: data) - .xAxisGrid(chartData: data) - .yAxisGrid(chartData: data) - .xAxisLabels(chartData: data) - .yAxisLabels(chartData: data) - .infoBox(chartData: data) - .headerBox(chartData: data) - .legends(chartData: data) +.touchOverlay(chartData: data) +.averageLine(chartData: data) +.yAxisPOI(chartData: data) +.xAxisGrid(chartData: data) +.yAxisGrid(chartData: data) +.xAxisLabels(chartData: data) +.yAxisLabels(chartData: data) +.infoBox(chartData: data) +.headerBox(chartData: data) +.legends(chartData: data) ``` */ public struct BarChart: View where ChartData: BarChartData { diff --git a/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift b/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift index cc81408b..8244ac19 100644 --- a/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift +++ b/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift @@ -13,8 +13,6 @@ import Foundation case line // Straight line from point to point case curvedLine // Dual control point curved line ``` - - - Tag: LineType */ public enum LineType { /// Straight line from point to point @@ -30,8 +28,6 @@ public enum LineType { case minimumWithMaximum(of: Double) // Set a custom baseline case zero // Set 0 as the lowest value ``` - - - Tag: Baseline */ public enum Baseline { /// Lowest value in the data set(s) @@ -49,8 +45,6 @@ public enum Baseline { case outline // Just stroke case filledOutLine // Both fill and stroke ``` - - - Tag: PointType */ public enum PointType { /// Just fill @@ -68,8 +62,6 @@ public enum PointType { case square case roundSquare ``` - - - Tag: PointShape */ public enum PointShape { /// Circle Shape @@ -83,11 +75,9 @@ public enum PointShape { /** Where the Y and X touch markers should attach themselves to. ``` - case line // Attached to the line. + case line(dot: Dot) // Attached to the line. case point // Attached to the data points. ``` - - - Tag: MarkerAttachemnt */ public enum MarkerAttachemnt { /// Attached to the line. @@ -100,22 +90,19 @@ public enum MarkerAttachemnt { Where the marker lines come from to meet at a specified point. ``` case none // No overlay markers. - case indicator // Rounded rectangle. - case vertical // Vertical line from top to bottom. - case full // 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. - + 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. ``` - - - Tag: LineMarkerType */ public enum LineMarkerType: MarkerType { /// No overlay markers. case none - /// Rounded rectangle. + /// Dot that follows the path. case indicator(style: DotStyle) /// Vertical line from top to bottom. case vertical(attachment: MarkerAttachemnt) @@ -138,8 +125,6 @@ public enum LineMarkerType: MarkerType { case none // No Dot case style(_ style: DotStyle) // Adds a dot the line at point of touch. ``` - - - Tag: Dot */ public enum Dot { /// No Dot diff --git a/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift b/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift index d06a286a..c676a77c 100644 --- a/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift @@ -9,6 +9,7 @@ import SwiftUI // MARK: - Paths extension Path { + /// Draws straight lines between data points. static func straightLine(rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, isFilled: Bool) -> Path { let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1) let y : CGFloat = rect.height / CGFloat(range) @@ -29,6 +30,7 @@ extension Path { return path } + /// Draws cubic BĆ©zier curved lines between data points. static func curvedLine(rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, isFilled: Bool) -> Path { let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1) let y : CGFloat = rect.height / CGFloat(range) diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index 8ec112db..9558cfb8 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -10,148 +10,39 @@ import SwiftUI /** Data for drawing and styling a single line, line chart. - This model contains all the data and styling information for a single line, line chart. + This model contains the data and styling information for a single line, line chart. # Example ``` - static func makeData() -> LineChartData { + static func weekOfData() -> LineChartData { let data = LineDataSet(dataPoints: [ - LineChartDataPoint(value: 20, xAxisLabel: "M", pointLabel: "Monday"), - LineChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"), + LineChartDataPoint(value: 120, xAxisLabel: "M", pointLabel: "Monday"), + LineChartDataPoint(value: 190, xAxisLabel: "T", pointLabel: "Tuesday"), LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), - LineChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"), + LineChartDataPoint(value: 175, xAxisLabel: "T", pointLabel: "Thursday"), LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), - LineChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") + LineChartDataPoint(value: 190, xAxisLabel: "S", pointLabel: "Sunday") ], - legendTitle: "Data", - pointStyle : PointStyle(), - style : LineStyle()) - - let metadata = ChartMetadata(title: "Some Data", subtitle: "A Week") - - let labels = ["Monday", "Thursday", "Sunday"] - - return LineChartData(dataSets : data, - metadata : metadata, - xAxisLabels : labels, - chartStyle : LineChartStyle(), - noDataText : Text("No Data")) + legendTitle: "Test One", + pointStyle: PointStyle(), + style: LineStyle(colour: Color.red, lineType: .curvedLine)) + + return LineChartData(dataSets : data, + metadata : ChartMetadata(title: "Some Data", subtitle: "A Week"), + xAxisLabels : ["Monday", "Thursday", "Sunday"], + chartStyle : LineChartStyle(infoBoxPlacement : .floating, + markerType : .indicator(style: DotStyle()), + xAxisLabelPosition : .bottom, + xAxisLabelsFrom : .chartData, + yAxisLabelPosition : .leading, + yAxisNumberOfLabels : 7, + baseline : .minimumWithMaximum(of: 80), + globalAnimation : .easeOut(duration: 1))) } ``` - - --- - - # Parts - - ## LineDataSet - ``` - LineDataSet(dataPoints: [LineChartDataPoint], - legendTitle: String, - pointStyle: PointStyle, - style: LineStyle) - ``` - ### LineChartDataPoint - ``` - LineChartDataPoint(value: Double, - xAxisLabel: String?, - pointLabel: String?, - date: Date?) - ``` - - ### PointStyle - ``` - PointStyle(pointSize: CGFloat, - borderColour: Color, - fillColour: Color, - lineWidth: CGFloat, - pointType: PointType, - pointShape: PointShape) - ``` - - ### LineStyle - ``` - LineStyle(colour: Color, - ...) - - LineStyle(colours: [Color], - startPoint: UnitPoint, - endPoint: UnitPoint, - ...) - - LineStyle(stops: [GradientStop], - startPoint: UnitPoint, - endPoint: UnitPoint, - ...) - - LineStyle(..., - lineType: LineType, - strokeStyle: Stroke, - ignoreZero: Bool) - ``` - - ## ChartMetadata - ``` - ChartMetadata(title: String?, subtitle: String?) - ``` - - ## LineChartStyle - - ``` - LineChartStyle(infoBoxPlacement : InfoBoxPlacement, - infoBoxValueColour : Color, - infoBoxDescriptionColor : Color, - xAxisGridStyle : GridStyle, - xAxisLabelPosition : XAxisLabelPosistion, - xAxisLabelColour : Color, - xAxisLabelsFrom : LabelsFrom, - yAxisGridStyle : GridStyle, - yAxisLabelPosition : YAxisLabelPosistion, - yAxisLabelColour : Color, - yAxisNumberOfLabels : Int, - baseline : Baseline, - globalAnimation : Animation) - ``` - - ### GridStyle - ``` - GridStyle(numberOfLines: Int, - lineColour : Color, - lineWidth : CGFloat, - dash : [CGFloat], - dashPhase : CGFloat) - ``` - - --- - - # Also See - - [LineDataSet](x-source-tag://LineDataSet) - - [LineChartDataPoint](x-source-tag://LineChartDataPoint) - - [PointStyle](x-source-tag://PointStyle) - - [PointType](x-source-tag://PointType) - - [PointShape](x-source-tag://PointShape) - - [LineStyle](x-source-tag://LineStyle) - - [ColourType](x-source-tag://ColourType) - - [LineType](x-source-tag://LineType) - - [GradientStop](x-source-tag://GradientStop) - - [ChartMetadata](x-source-tag://ChartMetadata) - - [LineChartStyle](x-source-tag://LineChartStyle) - - [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) - - [GridStyle](x-source-tag://GridStyle) - - [XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) - - [LabelsFrom](x-source-tag://LabelsFrom) - - [YAxisLabelPosistion](x-source-tag://YAxisLabelPosistion) - - # Conforms to - - ObservableObject - - Identifiable - - LineChartDataProtocol - - LineAndBarChartData - - ChartData - - - Tag: LineChartData */ public final class LineChartData: LineChartDataProtocol, LegendProtocol { diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index 0a7b6219..6e460d50 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -17,159 +17,44 @@ import SwiftUI static func weekOfData() -> MultiLineChartData { let data = MultiLineDataSet(dataSets: [ - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 20, xAxisLabel: "M", pointLabel: "Monday"), - LineChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"), - LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), - LineChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"), - LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), - LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), - LineChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") - ], - legendTitle: "Test One", - pointStyle: PointStyle(), - style: LineStyle(colour: Color.red)), - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 90, xAxisLabel: "M", pointLabel: "Monday"), - LineChartDataPoint(value: 20, xAxisLabel: "T", pointLabel: "Tuesday"), - LineChartDataPoint(value: 120, xAxisLabel: "W", pointLabel: "Wednesday"), - LineChartDataPoint(value: 85, xAxisLabel: "T", pointLabel: "Thursday"), - LineChartDataPoint(value: 140, xAxisLabel: "F", pointLabel: "Friday"), - LineChartDataPoint(value: 10, xAxisLabel: "S", pointLabel: "Saturday"), - LineChartDataPoint(value: 20, xAxisLabel: "S", pointLabel: "Sunday") - ], - legendTitle: "Test Two", - pointStyle: PointStyle(), - style: LineStyle(colour: Color.blue))]) + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 60, xAxisLabel: "M", pointLabel: "Monday"), + LineChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"), + LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), + LineChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"), + LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), + LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), + LineChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") + ], + legendTitle: "Test One", + pointStyle: PointStyle(), + style: LineStyle(colour: Color.red)), + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 90, xAxisLabel: "M", pointLabel: "Monday"), + LineChartDataPoint(value: 60, xAxisLabel: "T", pointLabel: "Tuesday"), + LineChartDataPoint(value: 120, xAxisLabel: "W", pointLabel: "Wednesday"), + LineChartDataPoint(value: 85, xAxisLabel: "T", pointLabel: "Thursday"), + LineChartDataPoint(value: 140, xAxisLabel: "F", pointLabel: "Friday"), + LineChartDataPoint(value: 80, xAxisLabel: "S", pointLabel: "Saturday"), + LineChartDataPoint(value: 50, xAxisLabel: "S", pointLabel: "Sunday") + ], + legendTitle: "Test Two", + pointStyle: PointStyle(), + style: LineStyle(colour: Color.blue))]) - let metadata = ChartMetadata(title: "Some Data", subtitle: "A Week") - let labels = ["Monday", "Thursday", "Sunday"] - - return MultiLineChartData(dataSets : data, - metadata : metadata, - xAxisLabels : labels, - chartStyle : LineChartStyle(baseline: .zero), - noDataText : Text("No Data")) + return MultiLineChartData(dataSets: data, + metadata: ChartMetadata(title: "Some Data", subtitle: "A Week"), + xAxisLabels: ["Monday", "Thursday", "Sunday"], + chartStyle: LineChartStyle(infoBoxPlacement: .fixed, + markerType: .full(attachment: .line(dot: .style(DotStyle()))), + baseline: .minimumWithMaximum(of: 40))) } -} - - ``` - - --- - - # Parts - - ## LineDataSet - ``` - LineDataSet(dataPoints: [LineChartDataPoint], - legendTitle: String, - pointStyle: PointStyle, - style: LineStyle) - ``` - ### LineChartDataPoint ``` - LineChartDataPoint(value: Double, - xAxisLabel: String?, - pointLabel: String?, - date: Date?) - ``` - - ### PointStyle - ``` - PointStyle(pointSize: CGFloat, - borderColour: Color, - fillColour: Color, - lineWidth: CGFloat, - pointType: PointType, - pointShape: PointShape) - ``` - - ### LineStyle - ``` - LineStyle(colour: Color, - ...) - - LineStyle(colours: [Color], - startPoint: UnitPoint, - endPoint: UnitPoint, - ...) - - LineStyle(stops: [GradientStop], - startPoint: UnitPoint, - endPoint: UnitPoint, - ...) - - LineStyle(..., - lineType: LineType, - strokeStyle: Stroke, - ignoreZero: Bool) - ``` - - ## ChartMetadata - ``` - ChartMetadata(title: String?, subtitle: String?) - ``` - - ## LineChartStyle - - ``` - LineChartStyle(infoBoxPlacement : InfoBoxPlacement, - infoBoxValueColour : Color, - infoBoxDescriptionColor : Color, - xAxisGridStyle : GridStyle, - xAxisLabelPosition : XAxisLabelPosistion, - xAxisLabelColour : Color, - xAxisLabelsFrom : LabelsFrom, - yAxisGridStyle : GridStyle, - yAxisLabelPosition : YAxisLabelPosistion, - yAxisLabelColour : Color, - yAxisNumberOfLabels : Int, - baseline : Baseline, - globalAnimation : Animation) - ``` - - ### GridStyle - ``` - GridStyle(numberOfLines: Int, - lineColour : Color, - lineWidth : CGFloat, - dash : [CGFloat], - dashPhase : CGFloat) - ``` - - --- - - # Also See - - [LineDataSet](x-source-tag://LineDataSet) - - [LineChartDataPoint](x-source-tag://LineChartDataPoint) - - [PointStyle](x-source-tag://PointStyle) - - [PointType](x-source-tag://PointType) - - [PointShape](x-source-tag://PointShape) - - [LineStyle](x-source-tag://LineStyle) - - [ColourType](x-source-tag://ColourType) - - [LineType](x-source-tag://LineType) - - [GradientStop](x-source-tag://GradientStop) - - [ChartMetadata](x-source-tag://ChartMetadata) - - [LineChartStyle](x-source-tag://LineChartStyle) - - [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) - - [GridStyle](x-source-tag://GridStyle) - - [XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) - - [LabelsFrom](x-source-tag://LabelsFrom) - - [YAxisLabelPosistion](x-source-tag://YAxisLabelPosistion) - - # Conforms to - - ObservableObject - - Identifiable - - LineChartDataProtocol - - LineAndBarChartData - - ChartData - - - Tag: LineChartData */ public final class MultiLineChartData: LineChartDataProtocol, LegendProtocol { // MARK: - Properties - public let id : UUID = UUID() + public let id : UUID = UUID() @Published public var dataSets : MultiLineDataSet @Published public var metadata : ChartMetadata @@ -303,16 +188,6 @@ public final class MultiLineChartData: LineChartDataProtocol, LegendProtocol { } } } - -// public func getPointMarker() -> some View { -// ForEach(self.dataSets.dataSets, id: \.self) { dataSet in -// PointsSubView(dataSets : dataSet, -// minValue : self.getMinValue(), -// range : self.getRange(), -// animation : self.chartStyle.globalAnimation, -// isFilled : self.isFilled) -// } -// } // MARK: - Legends internal func setupLegends() { @@ -384,3 +259,4 @@ public final class MultiLineChartData: LineChartDataProtocol, LegendProtocol { public typealias Set = MultiLineDataSet public typealias DataPoint = LineChartDataPoint } + diff --git a/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift index 587015f0..37f9b2fd 100644 --- a/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift @@ -14,98 +14,40 @@ import SwiftUI # Example ``` - let data = LineDataSet(dataPoints: [ - LineChartDataPoint(value: 20, xAxisLabel: "M", pointLabel: "Monday"), - LineChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"), + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 120, xAxisLabel: "M", pointLabel: "Monday"), + LineChartDataPoint(value: 190, xAxisLabel: "T", pointLabel: "Tuesday"), LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), - LineChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"), + LineChartDataPoint(value: 175, xAxisLabel: "T", pointLabel: "Thursday"), LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), - LineChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") + LineChartDataPoint(value: 190, xAxisLabel: "S", pointLabel: "Sunday") ], - legendTitle: "Data", + legendTitle: "Test One", pointStyle: PointStyle(), - style: LineStyle()) + style: LineStyle(colour: Color.red, lineType: .curvedLine)) ``` - - # LineChartDataPoint - ``` - LineChartDataPoint(value: Double, - xAxisLabel: String?, - pointLabel: String?, - date: Date?) - ``` - - # PointStyle - ``` - PointStyle(pointSize: CGFloat, - borderColour: Color, - fillColour: Color, - lineWidth: CGFloat, - pointType: PointType, - pointShape: PointShape) - ``` - - # LineStyle - ``` - LineStyle(colour: Color, - ...) - - LineStyle(colours: [Color], - startPoint: UnitPoint, - endPoint: UnitPoint, - ...) - - LineStyle(stops: [GradientStop], - startPoint: UnitPoint, - endPoint: UnitPoint, - ...) - - LineStyle(..., - lineType: LineType, - strokeStyle: Stroke, - ignoreZero: Bool) - ``` - --- - # Also See - - [LineChartDataPoint](x-source-tag://LineChartDataPoint) - - [PointStyle](x-source-tag://PointStyle) - - [PointType](x-source-tag://PointType) - - [PointShape](x-source-tag://PointShape) - - [LineStyle](x-source-tag://LineStyle) - - [ColourType](x-source-tag://ColourType) - - [LineType](x-source-tag://LineType) - - [GradientStop](x-source-tag://GradientStop) - - # Conforms to - - SingleDataSet - - DataSet - - Hashable - - Identifiable - - - Tag: LineDataSet */ public struct LineDataSet: CTLineChartDataSet { - public let id : UUID + public let id : UUID = UUID() public var dataPoints : [LineChartDataPoint] public var legendTitle : String public var pointStyle : PointStyle public var style : LineStyle - /// Initialises a new data set for Line Chart. + /// Initialises a data set for a line in a Line Chart. /// - Parameters: /// - dataPoints: Array of elements. - /// - legendTitle: label for the data in legend. + /// - legendTitle: Label for the data in legend. /// - pointStyle: Styling information for the data point markers. - /// - style: Styling for how the line will be drawin. + /// - style: Styling for how the line will be draw in. public init(dataPoints : [LineChartDataPoint], legendTitle : String = "", pointStyle : PointStyle = PointStyle(), style : LineStyle = LineStyle() ) { - self.id = UUID() self.dataPoints = dataPoints self.legendTitle = legendTitle self.pointStyle = pointStyle diff --git a/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift index c15897c4..078514da 100644 --- a/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift @@ -8,120 +8,48 @@ import SwiftUI /** - Data set for a multiple lines + Data set containing multiple data sets for multiple lines Contains information about each of lines within the chart. # Example ``` - let data = MultiLineDataSet(dataSets: [ - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 60, xAxisLabel: "M", pointLabel: "Monday"), - LineChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"), - LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), - LineChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"), - LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), - LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), - LineChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") - ], - legendTitle: "Test One", - pointStyle: PointStyle(), - style: LineStyle(colour: Color.red)), - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 90, xAxisLabel: "M", pointLabel: "Monday"), - LineChartDataPoint(value: 60, xAxisLabel: "T", pointLabel: "Tuesday"), - LineChartDataPoint(value: 120, xAxisLabel: "W", pointLabel: "Wednesday"), - LineChartDataPoint(value: 85, xAxisLabel: "T", pointLabel: "Thursday"), - LineChartDataPoint(value: 140, xAxisLabel: "F", pointLabel: "Friday"), - LineChartDataPoint(value: 80, xAxisLabel: "S", pointLabel: "Saturday"), - LineChartDataPoint(value: 50, xAxisLabel: "S", pointLabel: "Sunday") - ], - legendTitle: "Test Two", - pointStyle: PointStyle(), - style: LineStyle(colour: Color.blue))]) +MultiLineDataSet(dataSets: [ + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 60, xAxisLabel: "M", pointLabel: "Monday"), + LineChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"), + LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), + LineChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"), + LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), + LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), + LineChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") + ], + legendTitle: "Test One", + pointStyle: PointStyle(), + style: LineStyle(colour: Color.red)), + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 90, xAxisLabel: "M", pointLabel: "Monday"), + LineChartDataPoint(value: 60, xAxisLabel: "T", pointLabel: "Tuesday"), + LineChartDataPoint(value: 120, xAxisLabel: "W", pointLabel: "Wednesday"), + LineChartDataPoint(value: 85, xAxisLabel: "T", pointLabel: "Thursday"), + LineChartDataPoint(value: 140, xAxisLabel: "F", pointLabel: "Friday"), + LineChartDataPoint(value: 80, xAxisLabel: "S", pointLabel: "Saturday"), + LineChartDataPoint(value: 50, xAxisLabel: "S", pointLabel: "Sunday") + ], + legendTitle: "Test Two", + pointStyle: PointStyle(), + style: LineStyle(colour: Color.blue)) + ]) ``` - - # DataSet - ``` - LineDataSet(dataPoints: [LineChartDataPoint], - legendTitle: String, - pointStyle: PointStyle, - style: LineStyle) - ``` - - - # LineChartDataPoint - ``` - LineChartDataPoint(value: Double, - xAxisLabel: String?, - pointLabel: String?, - date: Date?) - ``` - - - # PointStyle - ``` - PointStyle(pointSize: CGFloat, - borderColour: Color, - fillColour: Color, - lineWidth: CGFloat, - pointType: PointType, - pointShape: PointShape) - ``` - - # LineStyle - ``` - LineStyle(colour: Color, - ...) - - LineStyle(colours: [Color], - startPoint: UnitPoint, - endPoint: UnitPoint, - ...) - - LineStyle(stops: [GradientStop], - startPoint: UnitPoint, - endPoint: UnitPoint, - ...) - - LineStyle(..., - lineType: LineType, - strokeStyle: Stroke, - ignoreZero: Bool) - ``` - - --- - # Also See - - [LineDataSet](x-source-tag://LineDataSet) - - [LineChartDataPoint](x-source-tag://LineChartDataPoint) - - [PointStyle](x-source-tag://PointStyle) - - [PointType](x-source-tag://PointType) - - [PointShape](x-source-tag://PointShape) - - [LineStyle](x-source-tag://LineStyle) - - [ColourType](x-source-tag://ColourType) - - [LineType](x-source-tag://LineType) - - [GradientStop](x-source-tag://GradientStop) - - # Conforms to - - MultiDataSet - - DataSet - - Hashable - - Identifiable - - - - Tag: MultiLineDataSet */ public struct MultiLineDataSet: MultiDataSet { - public let id : UUID - + public let id : UUID = UUID() public var dataSets : [LineDataSet] - /// Initialises a new data set for Multiline Line Chart. + /// Initialises a new data set for multi-line Line Charts. public init(dataSets: [LineDataSet]) { - self.id = UUID() self.dataSets = dataSets } - } diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift index 58646505..e45e3b5d 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift @@ -12,23 +12,15 @@ import SwiftUI # Example ``` - LineChartDataPoint(value: 20, - xAxisLabel: "M", - pointLabel: "Monday", - date: Date()) + LineChartDataPoint(value : 20, + xAxisLabel : "M", + pointLabel : "Monday", + date : Date()) ``` - - # Conforms to - - CTLineAndBarDataPoint - - CTChartDataPoint - - Hashable - - Identifiable - - - Tag: LineChartDataPoint */ public struct LineChartDataPoint: CTLineAndBarDataPoint { - public let id = UUID() + public let id : UUID = UUID() public var value : Double public var xAxisLabel : String? diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index 3d41e6ce..d6c274d6 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -10,17 +10,12 @@ import SwiftUI // MARK: - Chart Data /** A protocol to extend functionality of `LineAndBarChartData` specifically for Line Charts. - - # Reference - [See LineAndBarChartData](x-source-tag://LineAndBarChartData) - - `LineAndBarChartData` conforms to [ChartData](x-source-tag://ChartData) - - - Tag: LineChartDataProtocol */ public protocol LineChartDataProtocol: LineAndBarChartData { + /// A type representing opaque View associatedtype Marker : View + /// A type representing opaque View associatedtype Points : View /** @@ -34,16 +29,33 @@ public protocol LineChartDataProtocol: LineAndBarChartData { - Parameters: - rect: Frame of the path. - - dataSet: Dataset used to draw the chart. + - dataPoints: Data points used to draw the chart. - touchLocation: Location of the touch or pointer input. + - lineType: Drawing style of the line. - Returns: The position to place the indicator. */ func getIndicatorLocation(rect: CGRect, dataPoints: [LineChartDataPoint], touchLocation: CGPoint, lineType: LineType) -> CGPoint + /** + Gets the location of a data point within the view. + - Parameters: + - touchLocation: Current location of the touch. + - chartSize: The size of the chart view as the parent view. + - dataSet: The data set to search in. + - Returns: The location on screen of data points. + */ func getSinglePoint(touchLocation: CGPoint, chartSize: GeometryProxy, dataSet: LineDataSet) -> CGPoint + /// Displays a view contatining touch markers. + /// - Parameters: + /// - dataSet: The data set to search in. + /// - touchLocation: Current location of the touch. + /// - chartSize: The size of the chart view as the parent view. + /// - Returns: Relevent touch marker based the chosen parameters. func markerSubView(dataSet: LineDataSet, touchLocation: CGPoint, chartSize: GeometryProxy) -> Marker + /// Displays Shapes over the data points. + /// - Returns: Relevent view containing point markers based the chosen parameters. func getPointMarker() -> Points } @@ -51,17 +63,12 @@ public protocol LineChartDataProtocol: LineAndBarChartData { // MARK: - Style /** A protocol to extend functionality of `CTLineAndBarChartStyle` specifically for Line Charts. - - - Tag: CTLineChartStyle */ public protocol CTLineChartStyle : CTLineAndBarChartStyle { /** Where to start drawing the line chart from. Zero or data set minium. - - [See Baseline](x-source-tag://Baseline) */ var baseline: Baseline { get set } - } @@ -69,13 +76,10 @@ public protocol CTLineChartStyle : CTLineAndBarChartStyle { // MARK: - DataSet /** A protocol to extend functionality of `SingleDataSet` specifically for Line Charts. - - # Reference - [See SingleDataSet](x-source-tag://SingleDataSet) - - - Tag: CTLineChartDataSet */ public protocol CTLineChartDataSet: SingleDataSet { + + /// A type representing colour styling associatedtype Styling : CTColourStyle /** @@ -87,10 +91,11 @@ public protocol CTLineChartDataSet: SingleDataSet { 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](x-source-tag://PointMarkers) + The markers are layed out when the ViewModifier `PointMarkers` is applied. */ var pointStyle : PointStyle { get set } diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index 19d7a981..2ac4e166 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -45,7 +45,7 @@ extension LineChartDataProtocol { Returns the relevent path based on the line type. - Parameters: - - style: Styling of the line. + - 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. @@ -70,8 +70,7 @@ extension LineChartDataProtocol { isFilled : isFilled) } } - // Maybe put all into extentions of: Path / CGPoint / CGFloat - // https://developer.apple.com/documentation/swiftui/path/element + /** How far along the path the touch or pointer is as a percent of the total. . @@ -90,6 +89,9 @@ extension LineChartDataProtocol { /** The total length of the path. + # Reference + https://developer.apple.com/documentation/swiftui/path/element + - Parameter path: Path to measure. - Returns: Total length of the path. */ @@ -220,10 +222,13 @@ extension LineChartDataProtocol { } - // https://swiftui-lab.com/swiftui-animations-part2/ + /** Returns a point on the path based on the X axis of the users touch input. + # Reference + https://swiftui-lab.com/swiftui-animations-part2/ + - Parameters: - percent: The distance along the path as a percentage. - path: Path to find location on. @@ -415,13 +420,20 @@ extension LineChartDataProtocol where Self.CTLineAndBarCS.Mark == LineMarkerType } } } +/** + Sub view for laying out and styling the indicator dot. + */ +internal struct IndicatorSwitch: View { -struct IndicatorSwitch: View { + private let indicator: Dot + private let location : CGPoint - let indicator: Dot - let location : CGPoint + internal init(indicator: Dot, location: CGPoint) { + self.indicator = indicator + self.location = location + } - var body: some View { + internal var body: some View { switch indicator { case .none: EmptyView() case .style(let style): diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift index 4d65c6eb..6bda153f 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift @@ -15,63 +15,26 @@ import SwiftUI # Example ``` - LineChartStyle(infoBoxPlacement : .header, - xAxisGridStyle : GridStyle(numberOfLines: 7, - lineColour : .gray, - lineWidth : 1, - dash : [8], - dashPhase : 0), + LineChartStyle(infoBoxPlacement : .floating, + markerType : .indicator(style: DotStyle()), + xAxisGridStyle : GridStyle(), xAxisLabelPosition : .bottom, - xAxisLabelsFrom : .dataPoint, - yAxisGridStyle : GridStyle(numberOfLines: 7, - lineColour : .gray, - lineWidth : 1, - dash : [8], - dashPhase : 0), + xAxisLabelColour : Color.primary, + xAxisLabelsFrom : .chartData, + yAxisGridStyle : GridStyle(), yAxisLabelPosition : .leading, - yAxisNumberOfLabels : 5, - baseline : .minimumValue, - globalAnimation : .linear(duration: 1)) + yAxisLabelColour : Color.primary, + yAxisNumberOfLabels : 7, + baseline : .minimumWithMaximum(of: 80), + globalAnimation : .easeOut(duration: 1)) ``` - # Options - ``` - LineChartStyle(infoBoxPlacement: InfoBoxPlacement, - infoBoxValueColour: Color, - infoBoxDescriptionColor: Color, - xAxisGridStyle: GridStyle, - xAxisLabelPosition: XAxisLabelPosistion, - xAxisLabelColour: Color, - xAxisLabelsFrom: LabelsFrom, - yAxisGridStyle: GridStyle, - yAxisLabelPosition: YAxisLabelPosistion, - yAxisLabelColour: Color, - yAxisNumberOfLabels: Int, - baseline: Baseline, - globalAnimation: Animation) - ``` - - --- - - # Also See - - [InfoBoxPlacement](x-source-tag://InfoBoxPlacement) - - [GridStyle](x-source-tag://GridStyle) - - [XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) - - [LabelsFrom](x-source-tag://LabelsFrom) - - [YAxisLabelPosistion](x-source-tag://YAxisLabelPosistion) - - # Conforms to - - CTLineChartStyle - - CTLineAndBarChartStyle - - CTChartStyle - - - Tag: LineChartStyle */ public struct LineChartStyle: CTLineChartStyle { public var infoBoxPlacement : InfoBoxPlacement public var infoBoxValueColour : Color - public var infoBoxDescriptionColor : Color + public var infoBoxDescriptionColour : Color public var markerType : LineMarkerType public var xAxisGridStyle : GridStyle @@ -91,7 +54,7 @@ public struct LineChartStyle: CTLineChartStyle { /// - Parameters: /// - infoBoxPlacement: Placement of the information box that appears on touch input. /// - infoBoxValueColour: Colour of the value part of the touch info. - /// - infoBoxDescriptionColor: Colour of the description part of the touch info. + /// - infoBoxDescriptionColour: Colour of the description part of the touch info. /// /// - markerType: Where the marker lines come from to meet at a specified point. /// @@ -106,10 +69,10 @@ public struct LineChartStyle: CTLineChartStyle { /// - yAxisNumberOfLabel: Number Of Labels on Y Axis. /// /// - baseline: Whether the chart is drawn from baseline of zero or the minimum datapoint value. - /// - globalAnimation: Gobal control of animations. + /// - globalAnimation: Global control of animations. public init(infoBoxPlacement : InfoBoxPlacement = .floating, infoBoxValueColour : Color = Color.primary, - infoBoxDescriptionColor : Color = Color.primary, + infoBoxDescriptionColour: Color = Color.primary, markerType : LineMarkerType = .indicator(style: DotStyle()), @@ -126,9 +89,9 @@ public struct LineChartStyle: CTLineChartStyle { baseline : Baseline = .minimumValue, globalAnimation : Animation = Animation.linear(duration: 1) ) { - self.infoBoxPlacement = infoBoxPlacement - self.infoBoxValueColour = infoBoxValueColour - self.infoBoxDescriptionColor = infoBoxDescriptionColor + self.infoBoxPlacement = infoBoxPlacement + self.infoBoxValueColour = infoBoxValueColour + self.infoBoxDescriptionColour = infoBoxDescriptionColour self.markerType = markerType diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift index 3fbfb396..64d66013 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift @@ -8,56 +8,16 @@ import SwiftUI /** - Model for controlling the aesthetic of the line chart. + Model for controlling the aesthetic of the line chart. # Example ``` - LineStyle(colour: .red, - lineType: .curvedLine, - strokeStyle: Stroke(lineWidth: 2, - lineCap: .round, - lineJoin: .round, - miterLimit: 10, - dash: [CGFloat](), - dashPhase: 0), - ignoreZero: false) + LineStyle(colour : .red, + lineType : .curvedLine, + strokeStyle: Stroke(lineWidth: 2)) ``` - --- - - # Options - - ``` - LineStyle(colour: Color, - ...) - - LineStyle(colours: [Color], - startPoint: UnitPoint, - endPoint: UnitPoint, - ...) - - LineStyle(stops: [GradientStop], - startPoint: UnitPoint, - endPoint: UnitPoint, - ...) - - LineStyle(..., - lineType: LineType, - strokeStyle: Stroke, - ignoreZero: Bool) - ``` - - # Also See - - [ColourType](x-source-tag://ColourType) - - [LineType](x-source-tag://LineType) - - [GradientStop](x-source-tag://GradientStop) - - # Conforms to - - CTColourStyle - - Hashable - - - Tag: LineStyle */ public struct LineStyle: CTColourStyle, Hashable { @@ -87,6 +47,7 @@ public struct LineStyle: CTColourStyle, Hashable { */ public var ignoreZero : Bool + // MARK: - Single colour /// Single Colour /// - Parameters: /// - colour: Single Colour @@ -95,12 +56,12 @@ public struct LineStyle: CTColourStyle, Hashable { /// - ignoreZero: Whether the chart should skip data points who's value is 0. public init(colour : Color = Color(.red), lineType : LineType = .curvedLine, - strokeStyle : Stroke = Stroke(lineWidth: 3, - lineCap: .round, - lineJoin: .round, + strokeStyle : Stroke = Stroke(lineWidth : 3, + lineCap : .round, + lineJoin : .round, miterLimit: 10, - dash: [CGFloat](), - dashPhase: 0), + dash : [CGFloat](), + dashPhase : 0), ignoreZero : Bool = false ) { self.colourType = .colour @@ -116,6 +77,7 @@ public struct LineStyle: CTColourStyle, Hashable { self.ignoreZero = ignoreZero } + // MARK: - Gradient colour /// Gradient Colour Line /// - Parameters: /// - colours: Colours for Gradient. @@ -150,6 +112,7 @@ public struct LineStyle: CTColourStyle, Hashable { self.ignoreZero = ignoreZero } + // MARK: - Gradient with stops /// Gradient with Stops Line /// - Parameters: /// - stops: Colours and Stops for Gradient with stop control. diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/PointStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/PointStyle.swift index 9bc42c7a..41671e74 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Style/PointStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Style/PointStyle.swift @@ -10,34 +10,17 @@ import SwiftUI /** Model for controlling the aesthetic of the point markers. - Point markers are placed on top of the line marking where the data points are. + Point markers are placed on top of the line, marking where the data points are. # Example ``` PointStyle(pointSize: 9, - borderColour: Color.primary, - fillColour: Color.red, - lineWidth: 2, pointType: .filledOutLine, + borderColour: .primary, + fillColour: .red, + lineWidth: 2, + pointType: .filledOutLine, pointShape: .circle) ``` - - # Options - ``` - PointStyle(pointSize: CGFloat, - borderColour: Color, - fillColour: Color, - lineWidth: CGFloat, - pointType: PointType, - pointShape: PointShape) - ``` - # Also See - - [PointType](x-source-tag://PointType) - - [PointShape](x-source-tag://PointShape) - - # Conforms to - - Hashable - - - Tag: PointStyle */ public struct PointStyle: Hashable { diff --git a/Sources/SwiftUICharts/LineChart/Shapes/LegendLine.swift b/Sources/SwiftUICharts/LineChart/Shapes/LegendLine.swift index 1f6a0d6d..07e80dc1 100644 --- a/Sources/SwiftUICharts/LineChart/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 1ba539d1..70199838 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Main line shape + */ internal struct LineShape: Shape { private let dataPoints : [LineChartDataPoint] @@ -16,7 +19,6 @@ internal struct LineShape: Shape { private let minValue : Double private let range : Double - internal init(dataPoints: [LineChartDataPoint], lineType : LineType, isFilled : Bool, diff --git a/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift index 23ecd61c..a72b0043 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Draws point markers over the data point locations. + */ internal struct Point: Shape where T: CTLineChartDataSet { private let dataSet : T @@ -25,43 +28,43 @@ internal struct Point: Shape where T: CTLineChartDataSet { internal func path(in rect: CGRect) -> Path { var path = Path() - lineChartDrawPoints(&path, rect, dataSet.dataPoints, minValue, range) - return path - } - - internal func lineChartDrawPoints(_ path: inout Path, _ rect: CGRect, _ dataPoints: [DP], _ minValue: Double, _ range: Double) { - - let x = rect.width / CGFloat(dataPoints.count-1) + 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(dataPoints[0].value - minValue) * -y) + rect.height) - 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) pointSwitch(&path, firstPoint) - for index in 1 ..< dataPoints.count - 1 { - let pointX : CGFloat = (CGFloat(index) * x) - dataSet.pointStyle.pointSize / CGFloat(2) - let pointY : CGFloat = ((CGFloat(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) - pointSwitch(&path, point) - } - + 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) + pointSwitch(&path, point) + } + - let lastPointX : CGFloat = (CGFloat(dataPoints.count-1) * x) - dataSet.pointStyle.pointSize / CGFloat(2) - let lastPointY : CGFloat = ((CGFloat(dataPoints[dataPoints.count-1].value - minValue) * -y) + rect.height) - dataSet.pointStyle.pointSize / CGFloat(2) + 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) 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: diff --git a/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift index ddbb1fd3..7a32e738 100644 --- a/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + ViewModifier for for laying out point markers. + */ internal struct PointMarkers: ViewModifier where T: LineChartDataProtocol { @ObservedObject var chartData: T @@ -47,13 +50,14 @@ extension View { # Unavailable for: - Bar Chart - Grouped Bar Chart + - Stacked Bar Chart - Pie Chart + - Multi Layer Pie Chart - Doughnut Chart - Parameter chartData: Chart data model. - Returns: A new view containing the chart with point markers. - - - Tag: PointMarkers + */ 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 index f3f0ba5f..32f655d9 100644 --- a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift @@ -7,6 +7,39 @@ 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) + .headerBox(chartData: data) + .legends(chartData: data) + ``` + */ public struct FilledLineChart: View where ChartData: LineChartData { @ObservedObject var chartData: ChartData @@ -14,6 +47,8 @@ public struct FilledLineChart: View where ChartData: LineChartData { 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.getMinValue() diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index 956725f0..3f190bd2 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -8,89 +8,40 @@ import SwiftUI /** - View for drawing a line graph. + View for drawing a line chart. - This creates a single line, line chart. + Uses `LineChartData` data model. - # Example - ## Data Initialisation - ``` - let data : LineChartData = makeData() - ``` - ## Declaration + # Declaration ``` LineChart(chartData: data) ``` - ## View Modifiers + # 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) - .yAxisPOI(chartData: data) - .pointMarkers(chartData: data) - .xAxisGrid(chartData: data) - .yAxisGrid(chartData: data) - .xAxisLabels(chartData: data) - .yAxisLabels(chartData: data) - .headerBox(chartData: data) - .legends(chartData: data) - ``` - - [Touch Overlay](x-source-tag://TouchOverlay) - - [Point Markers](x-source-tag://PointMarkers) - - [Average Line](x-source-tag://AverageLine) - - [Y Axis POI](x-source-tag://YAxisPOI) - - [X Axis Grid](x-source-tag://XAxisGrid) - - [Y Axis Grid](x-source-tag://YAxisGrid) - - [X Axis Labels](x-source-tag://XAxisLabels) - - [Y Axis Labels](x-source-tag://YAxisLabels) - - [Header Box](x-source-tag://HeaderBox) - - [Legends](x-source-tag://Legends) - - ## Data Model - `LineChartData` is the central model - ``` - static func makeData() -> LineChartData { - - let data = LineDataSet(dataPoints: [ - LineChartDataPoint(value: 20, xAxisLabel: "M", pointLabel: "Monday"), - LineChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"), - LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), - LineChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"), - LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), - LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), - LineChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") - ], - legendTitle: "Data", - pointStyle: PointStyle(), - style: LineStyle() - - let metadata = ChartMetadata(title: "Some Data", subtitle: "A Week") - - let labels = ["Monday", "Thursday", "Sunday"] - - return LineChartData(dataSets: data, - metadata: metadata, - xAxisLabels: labels, - chartStyle: LineChartStyle(), - calculations: .none) - } - + .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) + .headerBox(chartData: data) + .legends(chartData: data, columns: [GridItem(.flexible()), GridItem(.flexible())]) ``` - - --- - - # Also See - - [LineDataSet](x-source-tag://LineDataSet) - - [ChartMetadata](x-source-tag://ChartMetadata) - - [LineChartStyle](x-source-tag://LineChartStyle) - - # Conforms to - - View - - - Tag: ChartData */ public struct LineChart: View where ChartData: LineChartData { @@ -99,6 +50,8 @@ public struct LineChart: View where ChartData: LineChartData { private let minValue : Double private let range : Double + /// Initialises a line chart view. + /// - Parameter chartData: Must be LineChartData model. public init(chartData: ChartData) { self.chartData = chartData self.minValue = chartData.getMinValue() diff --git a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift index b9d4ddca..beaa330c 100644 --- a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift @@ -7,6 +7,39 @@ 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) + .headerBox(chartData: data) + .legends(chartData: data) + ``` + */ public struct MultiLineChart: View where ChartData: MultiLineChartData { @ObservedObject var chartData: ChartData @@ -14,6 +47,8 @@ public struct MultiLineChart: View where ChartData: MultiLineChartDat 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.getMinValue() diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift index a3d36ac9..4394ea1c 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift @@ -7,6 +7,12 @@ import SwiftUI +// 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: LineChartDataProtocol { private let chartData : CD @@ -15,9 +21,7 @@ internal struct LineChartColourSubView: View where CD: LineChartDataProtocol private let range : Double private let colour : Color private let isFilled : Bool - - - + internal init(chartData : CD, dataSet : LineDataSet, minValue : Double, @@ -47,7 +51,7 @@ internal struct LineChartColourSubView: View where CD: LineChartDataProtocol .fill(colour) }, else: { $0.trim(to: startAnimation ? 1 : 0) - .stroke(colour, style: Stroke.strokeToStrokeStyle(stroke: dataSet.style.strokeStyle)) + .stroke(colour, style: dataSet.style.strokeStyle.strokeToStrokeStyle()) }) .background(Color(.gray).opacity(0.01)) .if(chartData.viewData.hasXAxisLabels) { $0.xAxisBorder(chartData: chartData) } @@ -61,6 +65,12 @@ internal struct LineChartColourSubView: View where CD: LineChartDataProtocol } } +// 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: LineChartDataProtocol { private let chartData : CD @@ -114,7 +124,7 @@ internal struct LineChartColoursSubView: View where CD: LineChartDataProtoco .stroke(LinearGradient(gradient: Gradient(colors: colours), startPoint: startPoint, endPoint: endPoint), - style: Stroke.strokeToStrokeStyle(stroke: dataSet.style.strokeStyle)) + style: dataSet.style.strokeStyle.strokeToStrokeStyle()) }) @@ -130,10 +140,14 @@ internal struct LineChartColoursSubView: View where CD: LineChartDataProtoco } } +// 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: LineChartDataProtocol { - - private let chartData : CD private let dataSet : LineDataSet @@ -186,7 +200,7 @@ internal struct LineChartStopsSubView: View where CD: LineChartDataProtocol .stroke(LinearGradient(gradient: Gradient(stops: stops), startPoint: startPoint, endPoint: endPoint), - style: Stroke.strokeToStrokeStyle(stroke: dataSet.style.strokeStyle)) + style: dataSet.style.strokeStyle.strokeToStrokeStyle()) }) .background(Color(.gray).opacity(0.01)) diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift index 4206b75c..b0f75f79 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Sub view gets the point markers drawn, sets the styling and sets up the animations. + */ internal struct PointsSubView: View { private let dataSets: LineDataSet diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift new file mode 100644 index 00000000..d45db552 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift @@ -0,0 +1,79 @@ +// +// 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. + + # Example + ``` + static func makeData() -> DoughnutChartData { + let data = PieDataSet(dataPoints: [PieChartDataPoint(value: 7, pointDescription: "One", colour: .blue), + PieChartDataPoint(value: 2, pointDescription: "Two", colour: .red), + PieChartDataPoint(value: 9, pointDescription: "Three", colour: .purple), + PieChartDataPoint(value: 6, pointDescription: "Four", colour: .green), + PieChartDataPoint(value: 4, pointDescription: "Five", colour: .orange)], + legendTitle: "Data") + + return DoughnutChartData(dataSets: data, + metadata: ChartMetadata(title: "Pie", subtitle: "mmm doughnuts"), + chartStyle: DoughnutChartStyle(infoBoxPlacement: .header), + noDataText: Text("No Data")) + } + ``` + */ +public final class DoughnutChartData: DoughnutChartDataProtocol, LegendProtocol { + + public var id : UUID = UUID() + + @Published public var dataSets : PieDataSet + @Published public var metadata : ChartMetadata + @Published public var chartStyle : DoughnutChartStyle + @Published public var legends : [LegendData] + @Published public var infoView : InfoViewData + + public var noDataText: Text + public var chartType : (chartType: ChartType, dataSetType: DataSetType) + + // MARK: - Initializer + /// Initialises a Doughnut Chart. + /// + /// - 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 func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } + + internal func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } + + public typealias Set = PieDataSet + public typealias DataPoint = PieChartDataPoint + public typealias CTStyle = DoughnutChartStyle +} diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift new file mode 100644 index 00000000..0eee08d7 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift @@ -0,0 +1,145 @@ +// +// MultiLayerPieChartData.swift +// +// +// Created by Will Dale on 05/02/2021. +// + +import SwiftUI + +/** + Data for drawing and styling a multi layered pie chart. + + This model contains the data and styling information for a multi layered pie chart + + # Example + ``` + public static func makeData() -> MultiLayerPieChartData { + + let data = MultiPieDataSet(dataPoints: [ + MultiPieDataPoint(value: 40, pointDescription: "One", colour: Color(.red), + layerDataPoints: [ + MultiPieDataPoint(value: 50, colour: Color(.cyan), + layerDataPoints: [ + MultiPieDataPoint(value: 70, colour: .red, + layerDataPoints: [ + MultiPieDataPoint(value: 20, colour: .red), + MultiPieDataPoint(value: 30, colour: .blue) + ]), + MultiPieDataPoint(value: 30, colour: .blue, + layerDataPoints: [ + MultiPieDataPoint(value: 30, colour: .green), + MultiPieDataPoint(value: 50, colour: .orange) + ]) + ]), + MultiPieDataPoint(value: 70, colour: Color(.yellow), + layerDataPoints: [ + MultiPieDataPoint(value: 50, colour: .green, + layerDataPoints: [ + MultiPieDataPoint(value: 30, colour: .yellow), + MultiPieDataPoint(value: 30, colour: .pink) + ]), + MultiPieDataPoint(value: 30, colour: .red, + layerDataPoints: [ + MultiPieDataPoint(value: 50, colour: .green), + MultiPieDataPoint(value: 20, colour: .orange) + ]) + ]) + ]), + MultiPieDataPoint(value: 40, pointDescription: "Two", colour: Color(.blue), + layerDataPoints: [ + MultiPieDataPoint(value: 50, colour: Color(.cyan), + layerDataPoints: [ + MultiPieDataPoint(value: 70, colour: .red, + layerDataPoints: [ + MultiPieDataPoint(value: 60, colour: .green), + MultiPieDataPoint(value: 40, colour: .yellow) + ]), + MultiPieDataPoint(value: 30, colour: .blue, + layerDataPoints: [ + MultiPieDataPoint(value: 30, colour: .red), + MultiPieDataPoint(value: 20, colour: .orange) + ]) + ]), + MultiPieDataPoint(value: 70, colour: Color(.green), + layerDataPoints: [ + MultiPieDataPoint(value: 50, colour: .green, + layerDataPoints: [ + MultiPieDataPoint(value: 70, colour: .green), + MultiPieDataPoint(value: 60, colour: .pink) + ]), + MultiPieDataPoint(value: 30, colour: .red, + layerDataPoints: [ + MultiPieDataPoint(value: 10, colour: .orange), + MultiPieDataPoint(value: 50, colour: .pink) + ]) + ]) + ]) + ]) + return MultiLayerPieChartData(dataSets: data, + metadata: ChartMetadata(title: "Pie", subtitle: "mmm pie"), + chartStyle: PieChartStyle(infoBoxPlacement: .header)) + } + ``` + */ +public final class MultiLayerPieChartData: MultiPieChartDataProtocol { + + public var id : UUID = UUID() + @Published public var dataSets : MultiPieDataSet + @Published public var metadata : ChartMetadata + @Published public var chartStyle : PieChartStyle + @Published public var legends : [LegendData] + @Published public var infoView : InfoViewData + + public var noDataText: Text + public var chartType : (chartType: ChartType, dataSetType: DataSetType) + + // MARK: - Initializer + /// Initialises a multi layered pie chart. + /// + /// - 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 : MultiPieDataSet, + 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 func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } + + + + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [MultiPieDataPoint] { + let points : [MultiPieDataPoint] = [] + return points + } + + public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { + return [HashablePoint(x: touchLocation.x, y: touchLocation.y)] + } + + internal func setupLegends() {} + + internal func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } + + public typealias Set = MultiPieDataSet + public typealias DataPoint = MultiPieDataPoint + public typealias CTStyle = PieChartStyle +} diff --git a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift similarity index 50% rename from Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift rename to Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift index 2efbd6ef..5b93e720 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift @@ -7,9 +7,31 @@ import SwiftUI +/** + Data for drawing and styling a pie chart. + + This model contains the data and styling information for a pie chart. + + # Example + ``` + static func makeData() -> PieChartData { + let data = PieDataSet(dataPoints: [ + PieChartDataPoint(value: 7, pointDescription: "One", colour: .blue), + PieChartDataPoint(value: 2, pointDescription: "Two", colour: .red), + PieChartDataPoint(value: 9, pointDescription: "Three", colour: .purple), + PieChartDataPoint(value: 6, pointDescription: "Four", colour: .green), + PieChartDataPoint(value: 4, pointDescription: "Five", colour: .orange)], + legendTitle: "Data") + + return PieChartData(dataSets: data, + metadata: ChartMetadata(title: "Pie", subtitle: "mmm pie"), + chartStyle: PieChartStyle(infoBoxPlacement: .header)) + } + ``` + */ public final class PieChartData: PieChartDataProtocol, LegendProtocol { - @Published public var id : UUID = UUID() + public var id : UUID = UUID() @Published public var dataSets : PieDataSet @Published public var metadata : ChartMetadata @Published public var chartStyle : PieChartStyle @@ -19,10 +41,18 @@ public final class PieChartData: PieChartDataProtocol, LegendProtocol { public var noDataText: Text public var chartType: (chartType: ChartType, dataSetType: DataSetType) + // MARK: - Initializer + /// Initialises a Pie Chart. + /// + /// - 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 + noDataText : Text = Text("No Data") ) { self.dataSets = dataSets self.metadata = metadata diff --git a/Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift b/Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift index 8e4e0f46..2105e423 100644 --- a/Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift +++ b/Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift @@ -7,12 +7,24 @@ import SwiftUI +/** + Data for a single segement of a pie chart. + + # Example + ``` + MultiPieDataPoint(value: 40, pointDescription: "One", colour: Color.red, + layerDataPoints: [ + MultiPieDataPoint(value: 5, colour: Color.blue) + ]) + + ``` + */ public struct MultiPieDataPoint: CTMultiPieChartDataPoint { - public var id: UUID = UUID() + public var id : UUID = UUID() // CTPieDataPoint - public var startAngle : Double = 0 - public var amount : Double = 0 + public var startAngle : Double = 0 + public var amount : Double = 0 // CTChartDataPoint public var value : Double public var pointDescription : String? @@ -22,6 +34,13 @@ public struct MultiPieDataPoint: CTMultiPieChartDataPoint { public var colour : Color + /// Data model for a single data point for a pie chart. + /// - Parameters: + /// - value: Value of the data point + /// - 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 of the segment. + /// - layerDataPoints: Optional data points for next layer out. public init(value : Double, pointDescription: String? = nil, date : Date? = nil, diff --git a/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift b/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift index 747fcb09..0a0d279e 100644 --- a/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift +++ b/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift @@ -7,6 +7,16 @@ import SwiftUI +/** + Data for a single segement of a pie chart. + + # Example + ``` + PieChartDataPoint(value: 7, + pointDescription: "One", + colour: .blue), + ``` + */ public struct PieChartDataPoint: CTPieDataPoint { public var id : UUID = UUID() @@ -19,6 +29,12 @@ public struct PieChartDataPoint: CTPieDataPoint { public var startAngle : Double = 0 public var amount : Double = 0 + /// Data model for a single data point for a pie chart. + /// - Parameters: + /// - value: Value of the data point + /// - 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 of the segment. public init(value : Double, pointDescription: String? = nil, date : Date? = nil, diff --git a/Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift b/Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift index c32a2413..f9699826 100644 --- a/Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift +++ b/Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift @@ -7,11 +7,30 @@ import SwiftUI +/** + Data set for drawing a multi layered pie chart. + + # Example + ``` + MultiPieDataSet(dataPoints: [ + MultiPieDataPoint(value: 30, colour: .red, layerDataPoints: [ + MultiPieDataPoint(value: 20, colour: .pink), + MultiPieDataPoint(value: 30, colour: .orange) + ]), + MultiPieDataPoint(value: 50, colour: .blue, layerDataPoints: [ + MultiPieDataPoint(value: 10, colour: .purple), + MultiPieDataPoint(value: 20, colour: .green) + ]) + ]) + ``` + */ public struct MultiPieDataSet: SingleDataSet { public var id: UUID = UUID() public var dataPoints : [MultiPieDataPoint] + /// Initialises a data set a multi layered pie chart. + /// - Parameter dataPoints: Array of elements. public init(dataPoints: [MultiPieDataPoint]) { self.dataPoints = dataPoints } diff --git a/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift b/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift index eb24537f..8a7531b9 100644 --- a/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift +++ b/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift @@ -7,23 +7,37 @@ import SwiftUI +/** + Data set for a pie chart. + + # Example + ``` + PieDataSet(dataPoints: [ + PieChartDataPoint(value: 7, pointDescription: "One", colour: .blue), + PieChartDataPoint(value: 2, pointDescription: "Two", colour: .red), + PieChartDataPoint(value: 9, pointDescription: "Three", colour: .purple), + PieChartDataPoint(value: 6, pointDescription: "Four", colour: .green), + PieChartDataPoint(value: 4, pointDescription: "Five", colour: .orange)], + legendTitle: "Data") + ``` + */ public struct PieDataSet: SingleDataSet { public var id : UUID = UUID() public var dataPoints : [PieChartDataPoint] public var legendTitle : String - public var style : PieSegmentStyle + /// 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, - style : PieSegmentStyle + legendTitle : String//, ) { self.dataPoints = dataPoints self.legendTitle = legendTitle - self.style = style } - public typealias Styling = PieSegmentStyle public typealias DataPoint = PieChartDataPoint } diff --git a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift deleted file mode 100644 index f4e60c74..00000000 --- a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartData.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// DoughnutChartData.swift -// -// -// Created by Will Dale on 02/02/2021. -// - -import SwiftUI - -public final class DoughnutChartData: DoughnutChartDataProtocol, LegendProtocol { - - @Published public var id : UUID = UUID() - @Published public var dataSets : PieDataSet - @Published public var metadata : ChartMetadata - @Published public var chartStyle : DoughnutChartStyle - @Published public var legends : [LegendData] - @Published public var infoView : InfoViewData - - public var noDataText: Text - public var chartType: (chartType: ChartType, dataSetType: DataSetType) - - 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 func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } - - internal func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } - - public typealias Set = PieDataSet - public typealias DataPoint = PieChartDataPoint - public typealias CTStyle = DoughnutChartStyle -} diff --git a/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift deleted file mode 100644 index 86cd4896..00000000 --- a/Sources/SwiftUICharts/PieChart/Models/MultiLayer/MultiLayerPieChartData.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// MultiLayerPieChartData.swift -// -// -// Created by Will Dale on 05/02/2021. -// - -import SwiftUI - -public final class MultiLayerPieChartData: MultiPieChartDataProtocol { - - @Published public var id : UUID = UUID() - @Published public var dataSets : MultiPieDataSet - @Published public var metadata : ChartMetadata - @Published public var chartStyle : PieChartStyle - @Published public var legends : [LegendData] - @Published public var infoView : InfoViewData - - public var noDataText: Text - public var chartType: (chartType: ChartType, dataSetType: DataSetType) - - public init(dataSets : MultiPieDataSet, - metadata : ChartMetadata, - chartStyle : PieChartStyle = PieChartStyle(), - 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 func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } - - - - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [MultiPieDataPoint] { - let points : [MultiPieDataPoint] = [] - return points - } - - public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - return [HashablePoint(x: touchLocation.x, y: touchLocation.y)] - } - - internal func setupLegends() {} - - internal func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } - - public typealias Set = MultiPieDataSet - public typealias DataPoint = MultiPieDataPoint - public typealias CTStyle = PieChartStyle -} diff --git a/Sources/SwiftUICharts/PieChart/Models/PieSegmentStyle.swift b/Sources/SwiftUICharts/PieChart/Models/PieSegmentStyle.swift deleted file mode 100644 index 6d0e01ba..00000000 --- a/Sources/SwiftUICharts/PieChart/Models/PieSegmentStyle.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// PieSegmentStyle.swift -// -// -// Created by Will Dale on 01/02/2021. -// - -import SwiftUI - -public struct PieSegmentStyle: 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? - - public init(colour : Color? = nil, - colours : [Color]? = nil, - stops : [GradientStop]? = nil, - startPoint : UnitPoint? = nil, - endPoint : UnitPoint? = nil - ) { - self.colourType = .colour - self.colour = colour - self.colours = colours - self.stops = stops - self.startPoint = startPoint - self.endPoint = endPoint - } -} diff --git a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolExtentions.swift similarity index 70% rename from Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift rename to Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolExtentions.swift index daaaa9a8..5a2882f6 100644 --- a/Sources/SwiftUICharts/PieChart/Models/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolExtentions.swift @@ -1,52 +1,19 @@ // -// PieChartProtocols.swift +// PieChartProtocolExtentions.swift // // -// Created by Will Dale on 02/02/2021. +// Created by Will Dale on 23/02/2021. // import SwiftUI -// MARK: - Chart Data -/** - A protocol to extend functionality of `ChartData` specifically for Pie and Doughnut Charts. - - # Reference - [See ChartData](x-source-tag://ChartData) - - - Tag: PieAndDoughnutChartDataProtocol - */ -public protocol PieAndDoughnutChartDataProtocol: ChartData {} - -/** - A protocol to extend functionality of `PieAndDoughnutChartDataProtocol` specifically for Pie Charts. - - # Reference - [See PieAndDoughnutChartDataProtocol](x-source-tag://PieAndDoughnutChartDataProtocol) - - - Tag: PieChartDataProtocol - */ -public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol {} - -/** - A protocol to extend functionality of `PieAndDoughnutChartDataProtocol` specifically for Doughnut Charts. - - # Reference - [See DoughnutChartDataProtocol](x-source-tag://DoughnutChartDataProtocol) - - - Tag: DoughnutChartDataProtocol - */ -public protocol DoughnutChartDataProtocol : PieAndDoughnutChartDataProtocol {} - -public protocol MultiPieChartDataProtocol : PieAndDoughnutChartDataProtocol {} - - - - -// MARK: - DataSet -public protocol CTMultiPieDataSet: DataSet {} - +// MARK: - Extentions extension PieAndDoughnutChartDataProtocol where Set == MultiPieDataSet, DataPoint == MultiPieDataPoint { + /** + 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 @@ -102,11 +69,13 @@ extension PieAndDoughnutChartDataProtocol where Set == MultiPieDataSet, DataPoin } } - -// * (180 / Double.pi) - extension PieAndDoughnutChartDataProtocol 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 @@ -145,10 +114,20 @@ extension PieAndDoughnutChartDataProtocol where Set == PieDataSet, DataPoint == } } } - + /** + 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 { - // http://www.cplusplus.com/reference/cmath/atan2/ - // https://stackoverflow.com/a/25398191 let center = CGPoint(x: rect.midX, y: rect.midY) let coordinates = CGPoint(x: touchLocation.x - center.x, y: touchLocation.y - center.y) @@ -161,61 +140,3 @@ extension PieAndDoughnutChartDataProtocol where Set == PieDataSet, DataPoint == } } } - - - - -// MARK: - DataPoints - -/** - A protocol to extend functionality of `CTChartDataPoint` specifically for Pie and Doughnut Charts. - - Currently empty. - - - Tag: CTPieDataPoint - */ -public protocol CTPieDataPoint: CTChartDataPoint { - var startAngle : Double { get set } - var amount : Double { get set } -} - -public protocol CTMultiPieChartDataPoint: CTChartDataPoint { - var layerDataPoints : [MultiPieDataPoint]? { get set } -} - - - - -// MARK: - Style -/** - A protocol to extend functionality of `CTChartStyle` specifically for Pie and Doughnut Charts. - - Currently empty. - - - Tag: CTPieAndDoughnutChartStyle - */ -public protocol CTPieAndDoughnutChartStyle: CTChartStyle {} - - -/** - A protocol to extend functionality of `CTPieAndDoughnutChartStyle` specifically for Pie Charts. - - Currently empty. - - - Tag: CTPieChartStyle - */ -public protocol CTPieChartStyle: CTPieAndDoughnutChartStyle {} - - -/** - A protocol to extend functionality of `CTPieAndDoughnutChartStyle` specifically for Doughnut Charts. - - - Tag: CTDoughnutChartStyle - */ -public protocol CTDoughnutChartStyle: CTPieAndDoughnutChartStyle { - - /** - Width / Delta of the Doughnut Chart - */ - var strokeWidth: CGFloat { get set } -} diff --git a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift new file mode 100644 index 00000000..d0fa2c85 --- /dev/null +++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift @@ -0,0 +1,98 @@ +// +// PieChartProtocols.swift +// +// +// Created by Will Dale on 02/02/2021. +// + +import SwiftUI + +// MARK: - Chart Data +/** + A protocol to extend functionality of `ChartData` specifically for Pie and Doughnut Charts. + */ +public protocol PieAndDoughnutChartDataProtocol: ChartData {} + +/** + A protocol to extend functionality of `PieAndDoughnutChartDataProtocol` specifically for Pie Charts. + */ +public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol {} + +/** + A protocol to extend functionality of `PieAndDoughnutChartDataProtocol` specifically for Doughnut Charts. + */ +public protocol DoughnutChartDataProtocol : PieAndDoughnutChartDataProtocol {} + +/** + A protocol to extend functionality of `PieAndDoughnutChartDataProtocol` specifically for multi layer Pie Charts. + */ +public protocol MultiPieChartDataProtocol : PieAndDoughnutChartDataProtocol {} + + + + +// MARK: - DataSet +public protocol CTMultiPieDataSet: DataSet {} + + + + + + + + +// MARK: - DataPoints + +/** + A protocol to extend functionality of `CTChartDataPoint` specifically for Pie and Doughnut Charts. + */ +public protocol CTPieDataPoint: CTChartDataPoint { + + /** + 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 } +} + +public protocol CTMultiPieChartDataPoint: CTChartDataPoint { + + /** + Second layer of data points. + */ + var layerDataPoints : [MultiPieDataPoint]? { 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/Doughnut/DoughnutChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift similarity index 54% rename from Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartStyle.swift rename to Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift index d4a6ae1b..3330f5ac 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Doughnut/DoughnutChartStyle.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift @@ -7,14 +7,22 @@ import SwiftUI -/// Model for controlling the overall aesthetic of the chart. +/** + Model for controlling the overall aesthetic of the chart. + + ``` + DoughnutChartStyle(infoBoxPlacement: .floating, + globalAnimation: .linear(duration: 1), + strokeWidth: 60) + ``` + */ public struct DoughnutChartStyle: CTDoughnutChartStyle { public var infoBoxPlacement : InfoBoxPlacement public var infoBoxValueColour : Color - public var infoBoxDescriptionColor : Color + public var infoBoxDescriptionColour : Color - public var globalAnimation : Animation + public var globalAnimation : Animation public var strokeWidth : CGFloat @@ -22,19 +30,19 @@ public struct DoughnutChartStyle: CTDoughnutChartStyle { /// - Parameters: /// - infoBoxPlacement: Placement of the information box that appears on touch input. /// - infoBoxValueColour: Colour of the value part of the touch info. - /// - infoBoxDescriptionColor: Colour of the description part of the touch info. - /// - globalAnimation: Gobal control of animations. + /// - infoBoxDescriptionColour: Colour of the description part 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, - infoBoxDescriptionColor : Color = Color.primary, + infoBoxDescriptionColour: Color = Color.primary, globalAnimation : Animation = Animation.linear(duration: 1), strokeWidth : CGFloat = 30 ) { - self.infoBoxPlacement = infoBoxPlacement - self.infoBoxValueColour = infoBoxValueColour - self.infoBoxDescriptionColor = infoBoxDescriptionColor - self.globalAnimation = globalAnimation - self.strokeWidth = strokeWidth + self.infoBoxPlacement = infoBoxPlacement + self.infoBoxValueColour = infoBoxValueColour + self.infoBoxDescriptionColour = infoBoxDescriptionColour + self.globalAnimation = globalAnimation + self.strokeWidth = strokeWidth } } diff --git a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift similarity index 51% rename from Sources/SwiftUICharts/PieChart/Models/Pie/PieChartStyle.swift rename to Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift index cc149e05..3acf5c60 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Pie/PieChartStyle.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift @@ -7,12 +7,21 @@ import SwiftUI -/// Model for controlling the overall aesthetic of the chart. +/** + Model for controlling the overall aesthetic of the chart. + + ``` + PieChartStyle(infoBoxPlacement: .fixed, + infoBoxValueColour: Color.primary, + infoBoxDescriptionColour: Color(.systemBackground), + globalAnimation: .linear(duration: 1)) + ``` + */ public struct PieChartStyle: CTPieChartStyle { public var infoBoxPlacement : InfoBoxPlacement public var infoBoxValueColour : Color - public var infoBoxDescriptionColor : Color + public var infoBoxDescriptionColour : Color public var globalAnimation : Animation @@ -20,17 +29,17 @@ public struct PieChartStyle: CTPieChartStyle { /// - Parameters: /// - infoBoxPlacement: Placement of the information box that appears on touch input. /// - infoBoxValueColour: Colour of the value part of the touch info. - /// - infoBoxDescriptionColor: Colour of the description part of the touch info. - /// - globalAnimation: Gobal control of animations. + /// - infoBoxDescriptionColour: Colour of the description part of the touch info. + /// - globalAnimation: Global control of animations. public init(infoBoxPlacement : InfoBoxPlacement = .floating, infoBoxValueColour : Color = Color.primary, - infoBoxDescriptionColor : Color = Color.primary, + infoBoxDescriptionColour: Color = Color.primary, globalAnimation : Animation = Animation.linear(duration: 1) ) { - self.infoBoxPlacement = infoBoxPlacement - self.infoBoxValueColour = infoBoxValueColour - self.infoBoxDescriptionColor = infoBoxDescriptionColor - self.globalAnimation = globalAnimation + self.infoBoxPlacement = infoBoxPlacement + self.infoBoxValueColour = infoBoxValueColour + self.infoBoxDescriptionColour = infoBoxDescriptionColour + 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 index 085de5f3..eb4b7f79 100644 --- a/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift +++ b/Sources/SwiftUICharts/PieChart/Shapes/PieSegmentShape.swift @@ -7,14 +7,15 @@ 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 @@ -30,33 +31,3 @@ internal struct PieSegmentShape: Shape, Identifiable { return path } } - - -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/Views/DoughnutChart.swift b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift index 0baba021..9d5510bb 100644 --- a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift @@ -7,10 +7,33 @@ 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) + .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 } diff --git a/Sources/SwiftUICharts/PieChart/Views/MultiLayerPie.swift b/Sources/SwiftUICharts/PieChart/Views/MultiLayerPieChart.swift similarity index 78% rename from Sources/SwiftUICharts/PieChart/Views/MultiLayerPie.swift rename to Sources/SwiftUICharts/PieChart/Views/MultiLayerPieChart.swift index 8474ca2d..7bdcfaab 100644 --- a/Sources/SwiftUICharts/PieChart/Views/MultiLayerPie.swift +++ b/Sources/SwiftUICharts/PieChart/Views/MultiLayerPieChart.swift @@ -7,10 +7,33 @@ import SwiftUI -public struct MultiLayerPie: View where ChartData: MultiLayerPieChartData { +/** + View for creating a multi layer pie chart. + + Uses `MultiLayerPieChartData` data model. + + # Declaration + ``` + MultiLayerPieChart(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) + .headerBox(chartData: data) + .legends(chartData: data) + ``` + */ +public struct MultiLayerPieChart: View where ChartData: MultiLayerPieChartData { @ObservedObject var chartData: ChartData + /// Initialises a bar chart view. + /// - Parameter chartData: Must be MultiLayerPieChartData. public init(chartData: ChartData) { self.chartData = chartData } diff --git a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift index 12337459..e09b65fb 100644 --- a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift @@ -7,10 +7,33 @@ 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) + .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 } diff --git a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift index a065ebb3..e5aefc09 100644 --- a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift +++ b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift @@ -7,25 +7,48 @@ import Foundation -struct DataFunctions { +/** + A collection of functions for getting infomation about the data sets. +*/ + struct DataFunctions { // MARK: - Single Data Set - static func dataSetMaxValue(from dataSets: T) -> Double { - return dataSets.dataPoints.max { $0.value < $1.value }?.value ?? 0 + /** + Returns the highest value in the data set. + - Parameter dataSet: Target data set. + - Returns: Highest value in data set. + */ + static func dataSetMaxValue(from dataSet: T) -> Double { + return dataSet.dataPoints.max { $0.value < $1.value }?.value ?? 0 } - static func dataSetMinValue(from dataSets: T) -> Double { - return dataSets.dataPoints.min { $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. + */ + static func dataSetMinValue(from dataSet: T) -> Double { + return dataSet.dataPoints.min { $0.value < $1.value }?.value ?? 0 } - static func dataSetAverage(from dataSets: T) -> Double { - let sum = dataSets.dataPoints.reduce(0) { $0 + $1.value } - return sum / Double(dataSets.dataPoints.count) + /** + Returns the average value from the data set. + - Parameter dataSet: Target data set. + - Returns: Average of values in data set. + */ + static func dataSetAverage(from dataSet: T) -> Double { + let sum = dataSet.dataPoints.reduce(0) { $0 + $1.value } + return sum / Double(dataSet.dataPoints.count) } - static func dataSetRange(from dataSets: T) -> Double { - let maxValue = dataSets.dataPoints.max { $0.value < $1.value }?.value ?? 0 - let minValue = dataSets.dataPoints.min { $0.value < $1.value }?.value ?? 0 + /** + Returns the difference between the highest and lowest numbers in the data set. + - Parameter dataSet: Target data set. + - Returns: Difference between the highest and lowest values in data set. + */ + static func dataSetRange(from dataSet: T) -> Double { + let maxValue = dataSet.dataPoints.max { $0.value < $1.value }?.value ?? 0 + let minValue = dataSet.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 @@ -36,6 +59,11 @@ struct DataFunctions { // MARK: - Multi Data Sets + /** + Returns the highest value in the data sets + - Parameter dataSet: Target data sets. + - Returns: Highest value in data sets. + */ static func multiDataSetMaxValue(from dataSets: T) -> Double { var setHolder : [Double] = [] for set in dataSets.dataSets { @@ -44,6 +72,11 @@ struct DataFunctions { 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. + */ static func multiDataSetMinValue(from dataSets: T) -> Double { var setHolder : [Double] = [] for set in dataSets.dataSets { @@ -52,6 +85,11 @@ struct DataFunctions { 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. + */ static func multiDataSetAverage(from dataSets: T) -> Double { var setHolder : [Double] = [] for set in dataSets.dataSets { @@ -62,6 +100,11 @@ struct DataFunctions { return sum / Double(setHolder.count) } + /** + Returns the difference between the highest and lowest numbers in the data sets. + - Parameter dataSet: Target data sets. + - Returns: Difference between the highest and lowest values in data sets. + */ static func multiDataSetRange(from dataSets: T) -> Double { var setMaxHolder : [Double] = [] for set in dataSets.dataSets { diff --git a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift index e6495192..cd9191f0 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift @@ -7,18 +7,26 @@ 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. + + # Reference + [SO](https://stackoverflow.com/a/62962375) + */ + @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Transform) -> some View { if condition { transform(self) } else { self } } } -// https://fivestars.blog/swiftui/conditional-modifiers.html + extension View { - @ViewBuilder - func `ifElse`(_ condition: Bool, + /** + 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 { @@ -31,8 +39,13 @@ extension View { } -// 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) { @@ -41,6 +54,11 @@ extension 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 { return onDisappear { withAnimation(animation) { @@ -51,6 +69,7 @@ extension View { } extension Color { + /// Returns the relevant system background colour for the device. public static var systemsBackground: Color { #if os(iOS) return Color(.systemBackground) diff --git a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift index fa675d74..8819f8a5 100644 --- a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift @@ -14,8 +14,6 @@ import Foundation case single // Single data set - i.e LineDataSet case multi // Multi data set - i.e MultiLineDataSet ``` - - - Tag: DataSetType */ public enum DataSetType { case single @@ -29,8 +27,6 @@ public enum DataSetType { case bar // Bar Chart Type case pie // Pie Chart Type ``` - - - Tag: ChartType */ public enum ChartType { /// Line Chart Type @@ -49,8 +45,6 @@ public enum ChartType { case gradientColour // Colour Gradient case gradientStops // Colour Gradient with stop control ``` - - - Tag: ColourType */ public enum ColourType { /// Single Colour @@ -69,8 +63,6 @@ public enum ColourType { case fixed // Centered in view. case header // Fix in the Header box. Must have .headerBox(). ``` - - - Tag: InfoBoxPlacement */ public enum InfoBoxPlacement { /// Follows input across the chart. diff --git a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift index 8badb1e5..494833e0 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift @@ -10,14 +10,12 @@ import SwiftUI /** Data model for the chart's metadata - Contains the Title, Subtitle and Title for Legend. + Contains the Title, Subtitle and colour information for them. # Example ``` - let metadata = ChartMetadata(title: "Some Data", subtitle: "A Week") + ChartMetadata(title: "Some Data", subtitle: "A weeks worth") ``` - - - Tag: ChartMetadata */ public struct ChartMetadata { /// The charts title diff --git a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift index 42299810..a80e22a8 100644 --- a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift @@ -7,42 +7,65 @@ import SwiftUI -/// Data model to pass view information internally so the layout can configure its self. -/// -/// # Reference -/// [CTChartDataPoint](x-source-tag://CTChartDataPoint) -/// -/// - Tag: InfoViewData +/** + Data model to pass view information internally for the `InfoBox` and `HeaderBox`. + */ public struct InfoViewData { /** - Is there currently input (touch or click) on the chart + Is there currently input (touch or click) on the chart. - Set from TouchOverlay + Set from TouchOverlay via the relevant protocol. - Used by TitleBox + Used by `HeaderBox` and `InfoBox`. */ var isTouchCurrent : Bool = false /** - Closest data point to input + Closest data points to input. - Set from TouchOverlay + Set from TouchOverlay via the relevant protocol. - Used by TitleBox + Used by `HeaderBox` and `InfoBox`. */ var touchOverlayInfo : [DP] = [] /** - Set specifier of data point readout + Set specifier of data point readout. - Set from TouchOverlay + Set from TouchOverlay via the relevant protocol. - Used by TitleBox + Used by `HeaderBox` and `InfoBox`. */ 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 `HeaderBox` and `InfoBox`. + */ var positionX : CGFloat = 0 + + /** + Current width of the `Info Box`. + + Used to set the location of the data point readout View. + + Set from TouchOverlay via the relevant protocol. + + Used by `HeaderBox` and `InfoBox`. + */ var frame : CGRect = .zero + + /** + Current width of the `YAxisLabels` + + Needed line up the touch overlay to compensate for + the loss of width. + */ var yAxisLabelWidth : CGFloat = 0 } diff --git a/Sources/SwiftUICharts/Shared/Models/LegendData.swift b/Sources/SwiftUICharts/Shared/Models/LegendData.swift index d7a59af2..1a3d2d05 100644 --- a/Sources/SwiftUICharts/Shared/Models/LegendData.swift +++ b/Sources/SwiftUICharts/Shared/Models/LegendData.swift @@ -7,9 +7,10 @@ import SwiftUI -/// Data model for Legends -/// - Tag: LegendData -public struct LegendData: CTColourStyle, Hashable, Identifiable { +/** + Data model to hold data for Legends + */ + public struct LegendData: CTColourStyle, Hashable, Identifiable { // MARK: - Parameters @@ -38,6 +39,7 @@ public struct LegendData: CTColourStyle, Hashable, Identifiable { /// - colour: Single Colour /// - 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 : Color, @@ -67,6 +69,7 @@ public struct LegendData: CTColourStyle, Hashable, Identifiable { /// - endPoint: End point for Gradient /// - 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, colours : [Color], @@ -98,6 +101,7 @@ public struct LegendData: CTColourStyle, Hashable, Identifiable { /// - endPoint: End point for Gradient /// - 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, stops : [GradientStop], diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 2864bc18..95d4a2a9 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -13,50 +13,45 @@ import SwiftUI Main protocol for passing data around library. All Chart Data models ultimately conform to this. - - - Tag: ChartData */ public protocol ChartData: ObservableObject, Identifiable { + /// A type representing a data set. -- `DataSet` associatedtype Set : DataSet + /// A type representing a data point. -- `CTChartDataPoint` associatedtype DataPoint: CTChartDataPoint + /// A type representing the chart style. -- `CTChartStyle` associatedtype CTStyle : CTChartStyle + /// A type representing opaque View associatedtype Touch : View var id: ID { get } /** Data model containing datapoints and styling information. - - `Set` is either `SingleData` or `MultiDataSet`. */ var dataSets: Set { get set } /** Data model containing the charts Title, Subtitle and the Title for Legend. - - # Reference - [ChartMetadata](x-source-tag://ChartMetadata) */ var metadata: ChartMetadata { get set } /** - Array of `LegendData` to populate the chart legend. + Array of `LegendData` to populate the charts legend. This is populated automatically from within each view. */ var legends: [LegendData] { get set } /** - Data model to hold temporary data from `TouchOverlay` ViewModifier and pass the data points to display in the `HeaderView`. + 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. - - # Reference - [CTChartStyle](x-source-tag://CTChartStyle) */ var chartStyle: CTStyle { get set } @@ -71,18 +66,12 @@ public protocol ChartData: ObservableObject, Identifiable { Allows for internal logic based on the type of chart. This might get removed in favour of a more protocol based approach. - - # Reference - [ChartType](x-source-tag://ChartType) - - [DataSetType](x-source-tag://DataSetType) - */ var chartType: (chartType: ChartType, dataSetType: DataSetType) { get } /** - Returns whether there are two or more dataPoints + Returns whether there are two or more data points. */ func isGreaterThanTwo() -> Bool @@ -94,8 +83,6 @@ public protocol ChartData: ObservableObject, Identifiable { - touchLocation: Current location of the touch. - chartSize: The size of the chart view as the parent view. - Returns: Array of data points. - - - Tag: getDataPoint */ func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [DataPoint] @@ -105,12 +92,9 @@ public protocol ChartData: ObservableObject, Identifiable { - 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. - - - Tag: getDataPoint */ func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] - /** Takes touch location and return a view based on the chart type and configuration. @@ -123,24 +107,19 @@ public protocol ChartData: ObservableObject, Identifiable { } - +/** + Protocol for dealing with legend data internally. + */ internal protocol LegendProtocol { /** Sets the order the Legends are layed out in. - Returns: Ordered array of Legends. - - # Reference - [LegendData](x-source-tag://LegendData) - - - Tag: legendOrder */ func legendOrder() -> [LegendData] /** Configures the legends based on the type of chart. - - - Tag: setupLegends */ func setupLegends() } @@ -150,9 +129,7 @@ internal protocol LegendProtocol { // MARK: - Data Sets /** - Main protocol set conformace for types of Data Sets. - - - Tag: DataSet + Main protocol to set conformace for types of Data Sets. */ public protocol DataSet: Hashable, Identifiable { var id : ID { get } @@ -160,16 +137,13 @@ public protocol DataSet: Hashable, Identifiable { /** Protocol for data sets that only require a single set of data . - - - Tag: SingleDataSet */ public protocol SingleDataSet: DataSet { + /// A type representing a data point. -- `CTChartDataPoint` associatedtype DataPoint : CTChartDataPoint /** Array of data points. - - [See CTChartDataPoint](x-source-tag://CTChartDataPoint) */ var dataPoints : [DataPoint] { get set } @@ -177,14 +151,13 @@ public protocol SingleDataSet: DataSet { /** Protocol for data sets that require a multiple sets of data . - - - Tag: MultiDataSet */ public protocol MultiDataSet: DataSet { + /// A type representing a single data set -- `SingleDataSet` associatedtype DataSet : SingleDataSet + /** - Array of DataSets. - [See SingleDataSet](x-source-tag://SingleDataSet) + Array of single data sets. */ var dataSets : [DataSet] { get set } } @@ -192,9 +165,6 @@ public protocol MultiDataSet: DataSet { // MARK: - Data Points /** Protocol to set base configuration for data points. - - - Tag: CTChartDataPoint - */ public protocol CTChartDataPoint: Hashable, Identifiable { @@ -206,17 +176,15 @@ public protocol CTChartDataPoint: Hashable, Identifiable { var value : Double { get set } /** - A laabel that can be displayed on touch input + A label that can be displayed on touch input It can eight be displayed in a floating box that tracks the users input location - or placed in the header. [See InfoBoxPlacement](x-source-tag://InfoBoxPlacement). + or placed in the header. */ var pointDescription : String? { get set } /** - Date can be used for performing additional calculations. - - [See Calculations](x-source-tag://Calculations) + Date can be used for optionally performing additional calculations. */ var date : Date? { get set } @@ -225,16 +193,11 @@ public protocol CTChartDataPoint: Hashable, Identifiable { // MARK: - Styles /** Protocol to set the styling data for the chart. - - - Tag: CTChartStyle */ public protocol CTChartStyle { /** Placement of the information box that appears on touch input. - - # Reference - [See InfoBoxPlacement](x-source-tag://InfoBoxPlacement) */ var infoBoxPlacement : InfoBoxPlacement { get set } @@ -246,7 +209,7 @@ public protocol CTChartStyle { /** Colour of the description part of the touch info. */ - var infoBoxDescriptionColor : Color { get set } + var infoBoxDescriptionColour : Color { get set } /** Global control of animations. @@ -260,18 +223,14 @@ public protocol CTChartStyle { /** - A protocol to set varius colour styles. + A protocol to set colour styling. Allows for single colour, gradient or gradient with stops control. - - - Tag: CTColourStyle */ public protocol CTColourStyle { /** Selection for the style of colour. - - [See ColourType](x-source-tag://ColourType) */ var colourType: ColourType { get set } @@ -284,9 +243,7 @@ public protocol CTColourStyle { /** Array of Gradient Stops. - GradientStop is a Hashable version of Gradient.Stop - - [See GradientStop](x-source-tag://GradientStop) + `GradientStop` is a Hashable version of Gradient.Stop */ var stops: [GradientStop]? { get set } diff --git a/Sources/SwiftUICharts/Shared/Types/GradientStop.swift b/Sources/SwiftUICharts/Shared/Types/GradientStop.swift index ed12b300..1b8d962d 100644 --- a/Sources/SwiftUICharts/Shared/Types/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 diff --git a/Sources/SwiftUICharts/Shared/Types/HashablePoint.swift b/Sources/SwiftUICharts/Shared/Types/HashablePoint.swift index bfd8461b..d42f1db9 100644 --- a/Sources/SwiftUICharts/Shared/Types/HashablePoint.swift +++ b/Sources/SwiftUICharts/Shared/Types/HashablePoint.swift @@ -9,6 +9,8 @@ import SwiftUI /** A hashable version of CGPoint + + CGPoint doesn't conform to Hashable. */ public struct HashablePoint: Hashable { diff --git a/Sources/SwiftUICharts/Shared/Types/Stroke.swift b/Sources/SwiftUICharts/Shared/Types/Stroke.swift index 2b0a1b25..f07c4f66 100644 --- a/Sources/SwiftUICharts/Shared/Types/Stroke.swift +++ b/Sources/SwiftUICharts/Shared/Types/Stroke.swift @@ -7,16 +7,21 @@ import SwiftUI -/// Replica of Apple's `StrokeStyle` that conforms to `Hashable` +/** + A hashable version of StrokeStyle + + StrokeStyle doesn't conform to Hashable. + */ public struct Stroke: Hashable, Identifiable { - public let id : UUID = UUID() - var lineWidth : CGFloat - var lineCap : CGLineCap - var lineJoin : CGLineJoin - var miterLimit : CGFloat - var dash : [CGFloat] - var dashPhase : CGFloat + 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, @@ -31,29 +36,29 @@ public struct Stroke: Hashable, Identifiable { self.miterLimit = miterLimit self.dash = dash self.dashPhase = dashPhase + } +} + +extension Stroke { + /// Convert `Stroke` to `StrokeStyle` + 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` - /// - 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) + func toStroke() -> Stroke { + Stroke(lineWidth : self.lineWidth, + lineCap : self.lineCap, + lineJoin : self.lineJoin, + miterLimit: self.miterLimit, + dash : self.dash, + dashPhase : self.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/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index b7267f3b..797d9446 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Displays the metadata about the chart as well as optionally touch overlay information. + */ internal struct HeaderBox: ViewModifier where T: ChartData { @ObservedObject var chartData: T @@ -36,7 +39,7 @@ internal struct HeaderBox: ViewModifier where T: ChartData { .foregroundColor(chartData.chartStyle.infoBoxValueColour) Text("\(info.pointDescription ?? "")") .font(.subheadline) - .foregroundColor(chartData.chartStyle.infoBoxDescriptionColor) + .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) } } else { Text("") @@ -96,7 +99,7 @@ internal struct HeaderBox: ViewModifier where T: ChartData { extension View { /** - Displays the metadata about the chart + Displays the metadata about the chart. Adds a view above the chart that displays the title and subtitle. infoBoxPlacement is set to .header then the datapoint info will diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift index 7b3e72e7..2c93071a 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + A view that displays information from `TouchOverlay`. + */ internal struct InfoBox: ViewModifier where T: ChartData { @ObservedObject var chartData: T @@ -32,7 +35,7 @@ internal struct InfoBox: ViewModifier where T: ChartData { selectedPoints : chartData.infoView.touchOverlayInfo, specifier : chartData.infoView.touchSpecifier, valueColour : chartData.chartStyle.infoBoxValueColour, - descriptionColour: chartData.chartStyle.infoBoxDescriptionColor, + descriptionColour: chartData.chartStyle.infoBoxDescriptionColour, boxFrame : $boxFrame) .position(x: setBoxLocationation(touchLocation: chartData.infoView.positionX, boxFrame : boxFrame, @@ -52,7 +55,7 @@ internal struct InfoBox: ViewModifier where T: ChartData { if let label = point.pointDescription { Text(label) .font(.subheadline) - .foregroundColor(chartData.chartStyle.infoBoxDescriptionColor) + .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) } } } @@ -95,6 +98,13 @@ internal struct InfoBox: ViewModifier where T: ChartData { } 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 58071b58..fe0c203a 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Displays legends under the chart. + */ internal struct Legends: ViewModifier where T: ChartData { @ObservedObject var chartData: T @@ -38,10 +41,11 @@ extension View { /** Displays legends under the chart. - - Parameter chartData: Chart data model. + - 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. - - - Tag: Legends */ 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/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index d241fd2b..175c18d5 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -9,30 +9,24 @@ import SwiftUI #if !os(tvOS) /** - Detects input either from touch of pointer. - Finds the nearest data point and displays the relevent information. - */ internal struct TouchOverlay: ViewModifier where T: ChartData { @ObservedObject var chartData: T - /// Current location of the touch input - @State private var touchLocation : CGPoint = CGPoint(x: 0, y: 0) - /// Frame information of the data point information box - @State private var boxFrame : CGRect = CGRect(x: 0, y: 0, width: 0, height: 50) - - /// Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information. - /// - Parameters: - /// - chartData: - /// - specifier: Decimal precision for labels internal init(chartData : T, specifier : String ) { self.chartData = chartData self.chartData.infoView.touchSpecifier = specifier } + + /// Current location of the touch input + @State private var touchLocation : CGPoint = CGPoint(x: 0, y: 0) + /// Frame information of the data point information box + @State private var boxFrame : CGRect = CGRect(x: 0, y: 0, width: 0, height: 50) + internal func body(content: Content) -> some View { Group { if chartData.isGreaterThanTwo() { @@ -95,6 +89,9 @@ extension View { If LineChartStyle --> infoBoxPlacement is set to .header then `.headerBox` is required. + If LineChartStyle --> infoBoxPlacement is set to .fixed or . floating + then `.infoBox` is required. + - Attention: Unavailable in tvOS @@ -102,8 +99,6 @@ extension View { - chartData: Chart data model. - specifier: Decimal precision for labels. - Returns: A new view containing the chart with a touch overlay. - - - Tag: TouchOverlay */ public func touchOverlay(chartData: T, specifier: String = "%.0f" diff --git a/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift b/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift index 31b47da3..d9fc6958 100644 --- a/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift +++ b/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + View to display text if there is not enough data to draw the chart. + */ public struct CustomNoDataView: View where T: ChartData { let chartData : T diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index d7d936de..a8b5cb35 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Sub view to setup and display the legends. + */ internal struct LegendView: View where T: ChartData { @ObservedObject var chartData : T @@ -47,6 +50,8 @@ internal struct LegendView: View where T: ChartData { } }.id(UUID()) } + + /// Detects whether to run the scale effect on the legend. private func scaleLegendBar(legend: LegendData) -> Bool { if chartData is BarChartData { @@ -65,6 +70,7 @@ internal struct LegendView: View where T: ChartData { 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 { @@ -78,10 +84,11 @@ internal struct LegendView: View where T: ChartData { } } + /// Returns a Line legend. func line(_ legend: LegendData) -> some View { Group { if let stroke = legend.strokeStyle { - let strokeStyle = Stroke.strokeToStrokeStyle(stroke: stroke) + let strokeStyle = stroke.strokeToStrokeStyle() if let colour = legend.colour { HStack { LegendLine(width: 40) @@ -122,6 +129,7 @@ internal struct LegendView: View where T: ChartData { } } + /// Returns a Bar legend. func bar(_ legend: LegendData) -> some View { Group { if let colour = legend.colour @@ -164,6 +172,7 @@ internal struct LegendView: View where T: ChartData { } } + /// Returns a Pie legend. func pie(_ legend: LegendData) -> some View { Group { if let colour = legend.colour { diff --git a/Sources/SwiftUICharts/Shared/Views/PosistionIndicator.swift b/Sources/SwiftUICharts/Shared/Views/PosistionIndicator.swift index 88291783..23bb8e7c 100644 --- a/Sources/SwiftUICharts/Shared/Views/PosistionIndicator.swift +++ b/Sources/SwiftUICharts/Shared/Views/PosistionIndicator.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + A dot that follows the line on touch events. + */ internal struct PosistionIndicator: View { private let fillColour : Color @@ -31,13 +34,22 @@ internal struct PosistionIndicator: View { } } +/** + 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, diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index bafe5c89..43f81542 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -7,6 +7,9 @@ import SwiftUI +/** +View that displays information from the touch events. + */ internal struct TouchOverlayBox: View { private var isTouchCurrent : Bool diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index 61300470..db349100 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -10,11 +10,6 @@ import SwiftUI // MARK: - Chart Data /** A protocol to extend functionality of `ChartData` specifically for Line and Bar Charts. - - # Reference - [See ChartData](x-source-tag://ChartData) - - - Tag: LineAndBarChartData */ public protocol LineAndBarChartData : ChartData { @@ -53,44 +48,31 @@ public protocol LineAndBarChartData : ChartData { data set (or 0) and highest number in the data set. - Returns: Array of evenly spaced numbers. - - - Tag: getYLabels */ func getYLabels() -> [Double] /** Returns the difference between the highest and lowest numbers in the data set or data sets. - - Tag: getRange */ func getRange() -> Double /** Returns the lowest value in the data set or data sets. - - Tag: getMinValue */ func getMinValue() -> Double /** Returns the highest value in the data set or data sets - - Tag: getMaxValue */ func getMaxValue() -> Double /** Returns the average value from the data set or data sets. - - Tag: getAverage */ func getAverage() -> Double /** Displays a view for the labels on the X Axis. - - Labels can come from either [CTChartDataPoint](x-source-tag://CTChartDataPoint) - or [ChartData](x-source-tag://ChartData) - - - Returns: An `HStack` of `Text` containin x axis labels. - - - Tag: getXAxidLabels */ func getXAxisLabels() -> XLabels } @@ -102,8 +84,6 @@ public protocol MarkerType {} /** A protocol to extend functionality of `CTChartStyle` specifically for Line and Bar Charts. - - - Tag: CTLineAndBarChartStyle */ public protocol CTLineAndBarChartStyle: CTChartStyle { @@ -172,8 +152,6 @@ public protocol CTLineAndBarChartStyle: CTChartStyle { // MARK: - DataPoints /** A protocol to extend functionality of `CTChartDataPoint` specifically for Line and Bar Charts. - - - Tag: CTLineAndBarDataPoint */ public protocol CTLineAndBarDataPoint: CTChartDataPoint { diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index 6a484a78..950fc513 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -12,7 +12,7 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData: T - private let uuid = UUID() + private let uuid : UUID = UUID() private let markerName : String private var markerValue : Double @@ -73,7 +73,7 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { chartData.legends.append(LegendData(id : uuid, legend : markerName, colour : lineColour, - strokeStyle : Stroke.strokeStyleToStroke(strokeStyle: strokeStyle), + strokeStyle : strokeStyle.toStroke(), prioity : 2, chartType : .line)) } From fdce653aa1607f02dec915d9ef4a1ebeb425bf99 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 24 Feb 2021 07:37:59 +0000 Subject: [PATCH 086/152] Add documentation. --- README.md | 2 +- .../Shared/Views/LegendView.swift | 2 +- .../Extras/LineAndBarEnums.swift | 9 +---- .../Models/ChartViewData.swift | 1 - .../SharedLineAndBar/Models/GridStyle.swift | 11 ------ .../Protocols/LineAndBarProtocols.swift | 35 ++++++------------- .../Shapes/DiamondShape.swift | 3 ++ .../Shapes/HorizontalGridShape.swift | 3 ++ .../SharedLineAndBar/Shapes/LabelShape.swift | 3 ++ .../Shapes/VerticalGridShape.swift | 3 ++ .../ViewModifiers/AxisBorders.swift | 6 ++++ .../ViewModifiers/XAxisGrid.swift | 7 ++-- .../ViewModifiers/XAxisLabels.swift | 7 ++-- .../ViewModifiers/YAxisGrid.swift | 7 ++-- .../ViewModifiers/YAxisLabels.swift | 8 +++-- .../ViewModifiers/YAxisPOI.swift | 12 ++++--- .../Views/HorizontalGridView.swift | 9 ++++- .../Views/VerticalGridView.swift | 3 ++ 18 files changed, 72 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 732399ed..653d75a4 100644 --- a/README.md +++ b/README.md @@ -526,7 +526,7 @@ ChartStyle(infoBoxPlacement : InfoBoxPlacement, - 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. +- globalAnimation: Global control of animations. ### GridStyle diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index a8b5cb35..34769703 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -33,7 +33,7 @@ internal struct LegendView: View where T: ChartData { switch legend.chartType { case .line: - + line(legend) case .bar: diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift b/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift index 019e9b09..f0e95363 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift @@ -14,13 +14,12 @@ Location of the X axis labels case top case bottom ``` - - - Tag: XAxisLabelPosistion */ public enum XAxisLabelPosistion { case top case bottom } + /** Where the label data come from. @@ -31,8 +30,6 @@ public enum XAxisLabelPosistion { case dataPoint // ChartData --> DataPoint --> xAxisLabel case chartData // ChartData --> xAxisLabels ``` - - - Tag: LabelsFrom */ public enum LabelsFrom { /// ChartData --> DataPoint --> xAxisLabel @@ -48,8 +45,6 @@ Location of the Y axis labels case leading case trailing ``` - - - Tag: YAxisLabelPosistion */ public enum YAxisLabelPosistion { case leading @@ -64,8 +59,6 @@ public enum YAxisLabelPosistion { case yAxis(specifier: String) // Places the label in the yAxis labels. case center(specifier: String) // Places the label in the center of chart. ``` - - - Tag: DisplayValue */ public enum DisplayValue { /// No label. diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift index 5f26f226..5e98b30f 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift @@ -8,7 +8,6 @@ import Foundation /// Data model to pass view information internally so the layout can configure its self. -/// - Tag: ChartViewData public struct ChartViewData { /// If the chart has labels on the X axis, the Y axis needs a different layout diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/GridStyle.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/GridStyle.swift index 91925764..6e91d668 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/GridStyle.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/GridStyle.swift @@ -18,17 +18,6 @@ import SwiftUI dash : [8], dashPhase : 0) ``` - - # Options - ``` - GridStyle(numberOfLines: Int, - lineColour : Color, - lineWidth : CGFloat, - dash : [CGFloat], - dashPhase : CGFloat) - ``` - - - Tag: GridStyle */ public struct GridStyle { diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index db349100..e6dd944b 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -13,14 +13,13 @@ import SwiftUI */ public protocol LineAndBarChartData : ChartData { + /// A type representing the chart style. -- `CTChartStyle` associatedtype CTLineAndBarCS : CTLineAndBarChartStyle + /// A type representing opaque View associatedtype XLabels : View /** Array of strings for the labels on the X Axis instead of the labels in the data points. - - To control where the labels should come from. - Set [LabelsFrom](x-source-tag://LabelsFrom) in [ChartStyle](x-source-tag://CTChartStyle). */ var xAxisLabels: [String]? { get set } @@ -35,9 +34,6 @@ public protocol LineAndBarChartData : ChartData { /** Data model conatining the style data for the chart. - - # Reference - [CTChartStyle](x-source-tag://CTChartStyle) */ var chartStyle: CTLineAndBarCS { get set } @@ -79,7 +75,9 @@ public protocol LineAndBarChartData : ChartData { // MARK: - Style - +/** + A protocol to get the correct touch overlay marker. + */ public protocol MarkerType {} /** @@ -87,6 +85,7 @@ public protocol MarkerType {} */ public protocol CTLineAndBarChartStyle: CTChartStyle { + /// A type representing touch overlay marker type. -- `MarkerType` associatedtype Mark : MarkerType /** @@ -95,50 +94,38 @@ public protocol CTLineAndBarChartStyle: CTChartStyle { var markerType : Mark { get set } /** - Style of the vertical lines breaking up the chart - - [See GridStyle](x-source-tag://GridStyle) + Style of the vertical lines breaking up the chart. */ var xAxisGridStyle: GridStyle { get set } /** - Location of the X axis labels - Top or Bottom - - [See XAxisLabelPosistion](x-source-tag://XAxisLabelPosistion) + 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 - - [See LabelsFrom](x-source-tag://LabelsFrom) + Where the label data come from. DataPoint or ChartData. */ var xAxisLabelsFrom: LabelsFrom { get set } /** Style of the horizontal lines breaking up the chart. - - [See GridStyle](x-source-tag://GridStyle) */ var yAxisGridStyle: GridStyle { get set } /** - Location of the X axis labels - Leading or Trailing - - [See YAxisLabelPosistion](x-source-tag://YAxisLabelPosistion) + 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 } @@ -158,5 +145,5 @@ public protocol CTLineAndBarDataPoint: CTChartDataPoint { /** Data points label for the X axis. */ - var xAxisLabel : String? { get set } + var xAxisLabel: String? { get set } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/DiamondShape.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/DiamondShape.swift index 7b8bb7ac..baa8f4e8 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/DiamondShape.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/DiamondShape.swift @@ -7,6 +7,9 @@ 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() diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/HorizontalGridShape.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/HorizontalGridShape.swift index d1716bb1..f88233f2 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/HorizontalGridShape.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/HorizontalGridShape.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Horizontal line. + */ internal struct HorizontalGridShape: Shape { internal func path(in rect: CGRect) -> Path { var path = Path() diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/LabelShape.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/LabelShape.swift index 0c039e97..711001cc 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/LabelShape.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/LabelShape.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Shape used in POI Markers when displaying value in the Y axid labels. + */ public struct LabelShape: Shape { public func path(in rect: CGRect) -> Path { var path = Path() diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/VerticalGridShape.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/VerticalGridShape.swift index 016c0fc1..8a4be76b 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/VerticalGridShape.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/VerticalGridShape.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Vertical line. + */ internal struct VerticalGridShape: Shape { internal func path(in rect: CGRect) -> Path { var path = Path() diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift index 7ac4d99b..a9a997f1 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Dividing line drawn between the X axis labels and the chart. + */ internal struct XAxisBorder: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData: T @@ -45,6 +48,9 @@ internal struct XAxisBorder: ViewModifier where T: LineAndBarChartData { } } +/** + Dividing line drawn between the Y axis labels and the chart. + */ internal struct YAxisBorder: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData: T diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift index f20d5639..5e6de77b 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Adds vertical lines along the X axis. + */ internal struct XAxisGrid: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData : T @@ -45,15 +48,15 @@ extension View { - Multi Line Chart - Bar Chart - Grouped Bar Chart + - Stacked Bar Chart # Unavailable for: - Pie Chart - Doughnut Chart + - Multi Layer Pie Chart - Parameter chartData: Chart data model. - Returns: A new view containing the chart with vertical lines under it. - - - Tag: XAxisGrid */ 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 index d4fb4053..302f01b6 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Labels for the X axis. + */ internal struct XAxisLabels: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData: T @@ -53,15 +56,15 @@ extension View { - Multi Line Chart - Bar Chart - Grouped Bar Chart + - Stacked Bar Chart # Unavailable for: - Pie Chart - Doughnut Chart + - Multi Layer Pie Chart - Parameter chartData: Chart data model. - Returns: A new view containing the chart with labels marking the x axis. - - - Tag: XAxisLabels */ 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 index 4068570f..9de9e0b0 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Adds horizontal lines along the X axis. + */ internal struct YAxisGrid: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData : T @@ -44,15 +47,15 @@ extension View { - Multi Line Chart - Bar Chart - Grouped Bar Chart + - Stacked Bar Chart # Unavailable for: - Pie Chart - Doughnut Chart + - Multi Layer Pie Chart - Parameter chartData: Chart data model. - Returns: A new view containing the chart with horizontal lines under it. - - - Tag: YAxisGrid */ 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 index c6a65b51..cd8061b3 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Automatically generated labels for the Y axis. + */ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData: T @@ -91,7 +94,7 @@ internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { extension View { /** - Automatically generated labels for the Y axis + Automatically generated labels for the Y axis. Controls are in ChartData --> ChartStyle @@ -103,15 +106,16 @@ extension View { - Multi Line Chart - Bar Chart - Grouped Bar Chart + - Stacked Bar Chart # Unavailable for: - Pie Chart - Doughnut Chart + - Multi Layer Pie Chart - Parameters: - specifier: Decimal precision specifier - Returns: HStack of labels - - Tag: YAxisLabels */ 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 index 950fc513..306b2f56 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -7,7 +7,9 @@ import SwiftUI -/// Configurable Point of interest +/** + Configurable Point of interest + */ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { @ObservedObject var chartData: T @@ -152,7 +154,7 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { extension View { /** - Horizontal line marking a custom value + Horizontal line marking a custom value. Shows a marker line at a specified value. @@ -181,10 +183,12 @@ extension View { - Multi Line Chart - Bar Chart - Grouped Bar Chart + - Stacked Bar Chart # Unavailable for: - Pie Chart - Doughnut Chart + - Multi Layer Pie Chart - Parameters: - chartData: Chart data model. @@ -196,8 +200,6 @@ extension View { - lineColour: Line Colour. - strokeStyle: Style of Stroke. - Returns: A new view containing the chart with a marker line at a specified value. - - - Tag: YAxisPOI */ public func yAxisPOI(chartData : T, markerName : String, @@ -250,10 +252,12 @@ extension View { - Multi Line Chart - Bar Chart - Grouped Bar Chart + - Stacked Bar Chart # Unavailable for: - Pie Chart - Doughnut Chart + - Multi Layer Pie Chart - Parameters: - chartData: Chart data model. diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift index f906f106..f7b3b70e 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift @@ -7,9 +7,16 @@ import SwiftUI +/** + Sub view of the Y axis grid view modifier. + */ internal struct HorizontalGridView: View where T: LineAndBarChartData { - var chartData : T + @ObservedObject private var chartData : T + + internal init(chartData: T) { + self.chartData = chartData + } @State private var startAnimation : Bool = false diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift index 3414e1b4..fe2f7694 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Sub view of the X axis grid view modifier. + */ internal struct VerticalGridView: View where T: LineAndBarChartData { @ObservedObject private var chartData : T From 67fee2c94ecbdfa11320901aa5657e8ef6f6d178 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 24 Feb 2021 14:12:21 +0000 Subject: [PATCH 087/152] Streamline getPointLocation. Revamp touch interaction. Move Touch into internal protocol. Add extensions to Classes. --- .../Models/ChartData/BarChartData.swift | 94 ++++---- .../ChartData/GroupedBarChartData.swift | 217 ++++++++++-------- .../ChartData/StackedBarChartData.swift | 148 ++++++------ .../BarChartProtocolsExtensions.swift | 2 +- .../BarChart/Views/GroupedBarChart.swift | 6 +- .../BarChart/Views/StackedBarChart.swift | 2 +- .../Views/SubViews/BarChartSubViews.swift | 14 +- .../Models/ChartData/LineChartData.swift | 92 +++----- .../Models/ChartData/MultiLineChartData.swift | 103 +++------ .../Models/Protocols/LineChartProtocols.swift | 12 +- .../LineChartProtocolsExtensions.swift | 92 +++++--- .../ViewModifiers/PointMarkers.swift | 4 +- .../LineChart/Views/FilledLineChart.swift | 4 +- .../LineChart/Views/LineChartView.swift | 4 +- .../LineChart/Views/MultiLineChart.swift | 4 +- .../Models/ChartData/DoughnutChartData.swift | 41 +++- .../ChartData/MultiLayerPieChartData.swift | 40 ++-- .../Models/ChartData/PieChartData.swift | 50 +++- .../PieChartProtocolExtentions.swift | 33 +-- .../Shared/Models/InfoViewData.swift | 6 +- .../Models/Protocols/SharedProtocols.swift | 44 ++-- .../Shared/Shapes/TouchOverlayMarker.swift | 24 -- .../Shared/Types/HashablePoint.swift | 24 -- .../Shared/ViewModifiers/InfoBox.swift | 4 +- .../Shared/ViewModifiers/TouchOverlay.swift | 35 +-- .../Protocols/LineAndBarProtocols.swift | 43 ++-- .../LineAndBarProtocolsExtentions.swift | 78 +++++-- .../ViewModifiers/XAxisLabels.swift | 8 +- .../ViewModifiers/YAxisPOI.swift | 9 +- 29 files changed, 642 insertions(+), 595 deletions(-) delete mode 100644 Sources/SwiftUICharts/Shared/Types/HashablePoint.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 301f5e21..6192f852 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -40,8 +40,8 @@ import SwiftUI } ``` */ -public final class BarChartData: BarChartDataProtocol, LegendProtocol { - // MARK: - Properties +public final class BarChartData: BarChartDataProtocol { + // MARK: Properties public let id : UUID = UUID() @Published public var dataSets : BarDataSet @@ -56,7 +56,7 @@ public final class BarChartData: BarChartDataProtocol, LegendProtocol { public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) - // MARK: - Initializer + // MARK: Initializer /// Initialises a standard Bar Chart. /// /// - Parameters: @@ -86,7 +86,7 @@ public final class BarChartData: BarChartDataProtocol, LegendProtocol { self.setupLegends() } - // MARK: - Labels + // MARK: Labels public func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { @@ -127,35 +127,22 @@ public final class BarChartData: BarChartDataProtocol, LegendProtocol { } } - // MARK: - Touch - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [BarChartDataPoint] { - var points : [BarChartDataPoint] = [] - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count) - let index : Int = Int((touchLocation.x) / xSection) - if index >= 0 && index < dataSets.dataPoints.count { - points.append(dataSets.dataPoints[index]) - } - return points - } - - public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - var locations : [HashablePoint] = [] - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count) - let ySection : CGFloat = chartSize.size.height / CGFloat(self.getMaxValue()) - let index : Int = Int((touchLocation.x) / xSection) - if index >= 0 && index < dataSets.dataPoints.count { - locations.append(HashablePoint(x: (CGFloat(index) * xSection) + (xSection / 2), - y: (chartSize.size.height - CGFloat(dataSets.dataPoints[index].value) * ySection))) - } - return locations + // MARK: Touch + public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + self.infoView.isTouchCurrent = true + self.infoView.touchLocation = touchLocation + self.infoView.chartSize = chartSize.frame(in: .local) + self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) } - public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { - let positions = self.getPointLocation(touchLocation: touchLocation, - chartSize: chartSize) - return ZStack { - ForEach(positions, id: \.self) { position in - + @ViewBuilder + public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { + + if let position = self.getPointLocation(dataSet: dataSets, + touchLocation: touchLocation, + chartSize: chartSize) { + + ZStack { switch self.chartStyle.markerType { case .none: EmptyView() @@ -179,10 +166,45 @@ public final class BarChartData: BarChartDataProtocol, LegendProtocol { .stroke(Color.primary, lineWidth: 2) } } + } else { EmptyView() } + } + + public typealias Set = BarDataSet + public typealias DataPoint = BarChartDataPoint + public typealias CTStyle = BarChartStyle +} + +// MARK: - Touch +extension BarChartData: TouchProtocol { + + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { + var points : [BarChartDataPoint] = [] + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count) + let index : Int = Int((touchLocation.x) / xSection) + if index >= 0 && index < dataSets.dataPoints.count { + points.append(dataSets.dataPoints[index]) } + self.infoView.touchOverlayInfo = points + } + + public func getPointLocation(dataSet: BarDataSet, touchLocation: CGPoint, chartSize: GeometryProxy) -> CGPoint? { + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count) + let ySection : CGFloat = chartSize.size.height / CGFloat(self.maxValue) + let index : Int = Int((touchLocation.x) / xSection) + if index >= 0 && index < dataSets.dataPoints.count { + return CGPoint(x: (CGFloat(index) * xSection) + (xSection / 2), + y: (chartSize.size.height - CGFloat(dataSets.dataPoints[index].value) * ySection)) + } + return nil + } +} + +// MARK: - Legends +extension BarChartData: LegendProtocol { + internal func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} } - // MARK: - Legends internal func setupLegends() { switch self.barStyle.colourFrom { @@ -261,12 +283,4 @@ public final class BarChartData: BarChartDataProtocol, LegendProtocol { } } } - - internal func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } - - public typealias Set = BarDataSet - public typealias DataPoint = BarChartDataPoint - public typealias CTStyle = BarChartStyle } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 9402ba13..81c4ab30 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -76,9 +76,9 @@ import SwiftUI } ``` */ -public final class GroupedBarChartData: MultiBarChartDataProtocol, LegendProtocol { +public final class GroupedBarChartData: MultiBarChartDataProtocol { - // MARK: - Properties + // MARK: Properties public let id : UUID = UUID() @Published public var dataSets : MultiBarDataSets @@ -95,8 +95,8 @@ public final class GroupedBarChartData: MultiBarChartDataProtocol, LegendProtoco public var chartType : (chartType: ChartType, dataSetType: DataSetType) var groupSpacing : CGFloat = 0 - - // MARK: - Initializer + + // MARK: Initializer /// Initialises a Grouped Bar Chart. /// /// - Parameters: @@ -128,7 +128,7 @@ public final class GroupedBarChartData: MultiBarChartDataProtocol, LegendProtoco self.setupLegends() } - // MARK: - Labels + // MARK: Labels @ViewBuilder public func getXAxisLabels() -> some View { switch self.chartStyle.xAxisLabelsFrom { @@ -172,8 +172,58 @@ public final class GroupedBarChartData: MultiBarChartDataProtocol, LegendProtoco } } - // MARK: - Touch - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [MultiBarChartDataPoint] { + // MARK: Touch + public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + self.infoView.isTouchCurrent = true + self.infoView.touchLocation = touchLocation + self.infoView.chartSize = chartSize.frame(in: .local) + self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) + } + + @ViewBuilder + public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { + + if let position = self.getPointLocation(dataSet: dataSets, + touchLocation: touchLocation, + chartSize: chartSize) { + ZStack { + + 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) + } + } + } else { EmptyView() } + } + + + public typealias Set = MultiBarDataSets + public typealias DataPoint = MultiBarChartDataPoint + public typealias CTStyle = BarChartStyle +} + +// MARK: - Touch +extension GroupedBarChartData: TouchProtocol { + + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { var points : [MultiBarChartDataPoint] = [] @@ -196,120 +246,87 @@ public final class GroupedBarChartData: MultiBarChartDataProtocol, LegendProtoco points.append(dataSet.dataPoints[subIndex]) } } - return points + self.infoView.touchOverlayInfo = points } - public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - var locations : [HashablePoint] = [] + public func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: GeometryProxy) -> CGPoint? { // Divide the chart into equal sections. - let superXSection : CGFloat = (chartSize.size.width / CGFloat(dataSets.dataSets.count)) + let superXSection : CGFloat = (chartSize.size.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(dataSets.dataSets.count - 1)) / CGFloat(dataSets.dataSets.count)) - + 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.size.width / CGFloat(dataSets.dataSets.count)) - compensation - let ySection : CGFloat = chartSize.size.height / CGFloat(self.getMaxValue()) - + let xSection : CGFloat = (chartSize.size.width / CGFloat(dataSet.dataSets.count)) - compensation + let ySection : CGFloat = chartSize.size.height / CGFloat(self.maxValue) + 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(index))) / xSubSection) - (dataSet.dataPoints.count * index) - - if subIndex >= 0 && subIndex < dataSet.dataPoints.count { + 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)) - locations.append(HashablePoint(x: element + section + spacing, - y: (chartSize.size.height - CGFloat(dataSet.dataPoints[subIndex].value) * ySection))) - - } - } - return locations - } - - public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { - let positions = self.getPointLocation(touchLocation: touchLocation, - chartSize: chartSize) - return ZStack { - ForEach(positions, id: \.self) { position in - - 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) - } + return CGPoint(x: element + section + spacing, + y: (chartSize.size.height - CGFloat(subDataSet.dataPoints[subIndex].value) * ySection)) + } } + + return nil } - - // MARK: - Legends +} + +// MARK: - Legends +extension GroupedBarChartData: LegendProtocol { + internal func setupLegends() { - + for group in self.groups { - - if group.colourType == .colour, - let colour = group.colour - { - self.legends.append(LegendData(id : group.id, - legend : group.title, - colour : colour, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if group.colourType == .gradientColour, - let colours = group.colours - { - self.legends.append(LegendData(id : group.id, - legend : group.title, - colours : colours, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if group.colourType == .gradientStops, - let stops = group.stops - { - self.legends.append(LegendData(id : group.id, - legend : group.title, - stops : stops, - startPoint : .leading, - endPoint : .trailing, - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } + + if group.colourType == .colour, + let colour = group.colour + { + self.legends.append(LegendData(id : group.id, + legend : group.title, + colour : colour, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if group.colourType == .gradientColour, + let colours = group.colours + { + self.legends.append(LegendData(id : group.id, + legend : group.title, + colours : colours, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if group.colourType == .gradientStops, + let stops = group.stops + { + self.legends.append(LegendData(id : group.id, + legend : group.title, + stops : stops, + startPoint : .leading, + endPoint : .trailing, + strokeStyle: nil, + prioity : 1, + chartType : .bar)) } + } } internal func legendOrder() -> [LegendData] { return legends.sorted { $0.prioity < $1.prioity} } - - public typealias Set = MultiBarDataSets - public typealias DataPoint = MultiBarChartDataPoint - public typealias CTStyle = BarChartStyle } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index b7cbd58b..e97a911f 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -72,9 +72,9 @@ import SwiftUI chartStyle: BarChartStyle(xAxisLabelsFrom: .dataPoint)) ``` */ -public final class StackedBarChartData: MultiBarChartDataProtocol, LegendProtocol { +public final class StackedBarChartData: MultiBarChartDataProtocol { - // MARK: - Properties + // MARK: Properties public let id : UUID = UUID() @Published public var dataSets : MultiBarDataSets @@ -90,7 +90,7 @@ public final class StackedBarChartData: MultiBarChartDataProtocol, LegendProtoco public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) - // MARK: - Initializer + // MARK: Initializer /// Initialises a Grouped Bar Chart. /// /// - Parameters: @@ -121,7 +121,7 @@ public final class StackedBarChartData: MultiBarChartDataProtocol, LegendProtoco self.chartType = (chartType: .bar, dataSetType: .multi) self.setupLegends() } - // MARK: - Labels + // MARK: Labels @ViewBuilder public func getXAxisLabels() -> some View { switch self.chartStyle.xAxisLabelsFrom { @@ -158,8 +158,57 @@ public final class StackedBarChartData: MultiBarChartDataProtocol, LegendProtoco } } - // MARK: - Touch - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [MultiBarChartDataPoint] { + // MARK: Touch + public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + self.infoView.isTouchCurrent = true + self.infoView.touchLocation = touchLocation + self.infoView.chartSize = chartSize.frame(in: .local) + self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) + } + + @ViewBuilder + public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { + + if let position = self.getPointLocation(dataSet: dataSets, + touchLocation: touchLocation, + chartSize: chartSize) { + ZStack { + + 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) + } + } + } else { EmptyView() } + } + + public typealias Set = MultiBarDataSets + public typealias DataPoint = MultiBarChartDataPoint + public typealias CTStyle = BarChartStyle +} + +// MARK: - Touch +extension StackedBarChartData: TouchProtocol { + + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { var points : [MultiBarChartDataPoint] = [] @@ -174,7 +223,7 @@ public final class StackedBarChartData: MultiBarChartDataProtocol, LegendProtoco // 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.getMaxValue() + let allMaxValue = self.maxValue let fraction : CGFloat = CGFloat(setMaxValue / allMaxValue) // Gets the height of each datapoint @@ -202,33 +251,31 @@ public final class StackedBarChartData: MultiBarChartDataProtocol, LegendProtoco } } } - return points + self.infoView.touchOverlayInfo = points } - public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - var locations : [HashablePoint] = [] - + public func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: GeometryProxy) -> CGPoint? { // Filter to get the right dataset based on the x axis. - let superXSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataSets.count) + let superXSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataSets.count) let superIndex : Int = Int((touchLocation.x) / superXSection) - - if superIndex >= 0 && superIndex < dataSets.dataSets.count { - - let dataSet = dataSets.dataSets[superIndex] - + + 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 = dataSet.dataPoints.max { $0.value < $1.value }?.value ?? 0 - let allMaxValue = self.getMaxValue() + 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 = dataSet.dataPoints.reduce(0) { $0 + $1.value } - dataSet.dataPoints.forEach { datapoint in + let sum = subDataSet.dataPoints.reduce(0) { $0 + $1.value } + subDataSet.dataPoints.forEach { datapoint in heightOfElements.append((chartSize.size.height * fraction) * CGFloat(datapoint.value / sum)) } - + // Gets the highest point of each element. var endPointOfElements : [CGFloat] = [] heightOfElements.enumerated().forEach { element in @@ -239,52 +286,23 @@ public final class StackedBarChartData: MultiBarChartDataProtocol, LegendProtoco endPointOfElements.append(returnValue) } - let yIndex = endPointOfElements.enumerated().first(where: { $0.element > abs(touchLocation.y - chartSize.size.height) }) - + let yIndex = endPointOfElements.enumerated().first(where: { + $0.element > abs(touchLocation.y - chartSize.size.height) + }) + if let index = yIndex?.offset { - if index >= 0 && index < dataSet.dataPoints.count { - - locations.append(HashablePoint(x: (CGFloat(superIndex) * superXSection) + (superXSection / 2), - y: (chartSize.size.height - endPointOfElements[index]))) - } - } - } - - return locations - } - - public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { - let positions = self.getPointLocation(touchLocation: touchLocation, - chartSize: chartSize) - return ZStack { - ForEach(positions, id: \.self) { position in - - 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) + if index >= 0 && index < subDataSet.dataPoints.count { + + return CGPoint(x: (CGFloat(superIndex) * superXSection) + (superXSection / 2), + y: (chartSize.size.height - endPointOfElements[index])) } } } + return nil } - +} + +extension StackedBarChartData: LegendProtocol { // MARK: - Legends internal func setupLegends() { for group in self.groups { @@ -327,8 +345,4 @@ public final class StackedBarChartData: MultiBarChartDataProtocol, LegendProtoco internal func legendOrder() -> [LegendData] { return legends.sorted { $0.prioity < $1.prioity} } - - public typealias Set = MultiBarDataSets - public typealias DataPoint = MultiBarChartDataPoint - public typealias CTStyle = BarChartStyle } diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift index e104d612..66d59859 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift @@ -11,7 +11,7 @@ import SwiftUI extension LineAndBarChartData where Self: BarChartDataProtocol { public func getYLabels() -> [Double] { var labels : [Double] = [Double]() - let maxValue: Double = self.getMaxValue() + let maxValue: Double = self.maxValue for index in 0...self.chartStyle.yAxisNumberOfLabels { labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) } diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index b24acd52..14aad6fc 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -63,7 +63,7 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD let colour = dataPoint.group.colour { - ColourBar(colour, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) } else if dataPoint.group.colourType == .gradientColour, let colours = dataPoint.group.colours, @@ -71,7 +71,7 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD let endPoint = dataPoint.group.endPoint { - GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) } else if dataPoint.group.colourType == .gradientStops, let stops = dataPoint.group.stops, @@ -81,7 +81,7 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) } } diff --git a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift index f8b5001e..b16a64df 100644 --- a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift @@ -55,7 +55,7 @@ public struct StackedBarChart: View where ChartData: StackedBarChartD ForEach(chartData.dataSets.dataSets) { dataSet in StackElementSubView(dataSet: dataSet) - .scaleEffect(y: startAnimation ? CGFloat(DataFunctions.dataSetMaxValue(from: dataSet) / chartData.getMaxValue()) : 0, anchor: .bottom) + .scaleEffect(y: startAnimation ? CGFloat(DataFunctions.dataSetMaxValue(from: dataSet) / chartData.maxValue) : 0, anchor: .bottom) .scaleEffect(x: chartData.barStyle.barWidth, anchor: .center) .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift index 42cff425..5256f3dc 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift @@ -28,7 +28,7 @@ internal struct BarChartDataSetSubView: View { let colour = chartData.barStyle.colour { - ColourBar(colour, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) } else if chartData.barStyle.colourType == .gradientColour, let colours = chartData.barStyle.colours, @@ -36,7 +36,7 @@ internal struct BarChartDataSetSubView: View { let endPoint = chartData.barStyle.endPoint { - GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) } else if chartData.barStyle.colourType == .gradientStops, let stops = chartData.barStyle.stops, @@ -46,7 +46,7 @@ internal struct BarChartDataSetSubView: View { let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) } } @@ -74,7 +74,7 @@ internal struct BarChartDataPointSubView: View { let colour = dataPoint.colour { - ColourBar(colour, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) } else if dataPoint.colourType == .gradientColour, let colours = dataPoint.colours, @@ -82,7 +82,7 @@ internal struct BarChartDataPointSubView: View { let endPoint = dataPoint.endPoint { - GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) } else if dataPoint.colourType == .gradientStops, let stops = dataPoint.stops, @@ -92,9 +92,9 @@ internal struct BarChartDataPointSubView: View { let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) } else { - ColourBar(.blue, dataPoint, chartData.getMaxValue(), chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + ColourBar(.blue, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) } } diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index 9558cfb8..c911fa0d 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -44,9 +44,9 @@ import SwiftUI ``` */ -public final class LineChartData: LineChartDataProtocol, LegendProtocol { +public final class LineChartData: LineChartDataProtocol { - // MARK: - Properties + // MARK: Properties public let id : UUID = UUID() @Published public var dataSets : LineDataSet @@ -61,7 +61,7 @@ public final class LineChartData: LineChartDataProtocol, LegendProtocol { public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) - // MARK: - Initializer + // MARK: Initializer /// Initialises a Single Line Chart. /// /// - Parameters: @@ -88,7 +88,7 @@ public final class LineChartData: LineChartDataProtocol, LegendProtocol { } // , calc : @escaping (LineDataSet) -> LineDataSet - // MARK: - Labels + // MARK: Labels public func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { @@ -111,7 +111,6 @@ public final class LineChartData: LineChartDataProtocol, LegendProtocol { } .padding(.horizontal, -4) - case .chartData: if let labelArray = self.xAxisLabels { HStack(spacing: 0) { @@ -132,50 +131,50 @@ public final class LineChartData: LineChartDataProtocol, LegendProtocol { } } } - - // MARK: - Points + // MARK: Points public func getPointMarker() -> some View { PointsSubView(dataSets : dataSets, - minValue : self.getMinValue(), - range : self.getRange(), + minValue : self.minValue, + range : self.range, animation : self.chartStyle.globalAnimation, isFilled : self.isFilled) } - // MARK: - Touch - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [LineChartDataPoint] { + // MARK: Touch + public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + self.infoView.isTouchCurrent = true + self.infoView.touchLocation = touchLocation + self.infoView.chartSize = chartSize.frame(in: .local) + self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) + } + + + + public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { + self.markerSubView(dataSet: self.dataSets, touchLocation: touchLocation, chartSize: chartSize) + } + + public typealias Set = LineDataSet + public typealias DataPoint = LineChartDataPoint +} + +// MARK: - Touch +extension LineChartData: TouchProtocol { + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { var points : [LineChartDataPoint] = [] let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count - 1) let index = Int((touchLocation.x + (xSection / 2)) / xSection) if index >= 0 && index < dataSets.dataPoints.count { points.append(dataSets.dataPoints[index]) } - return points + self.infoView.touchOverlayInfo = points } +} - public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - var locations : [HashablePoint] = [] - - let minValue : Double = self.getMinValue() - let range : Double = self.getRange() - - let ySection : CGFloat = chartSize.size.height / CGFloat(range) - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count - 1) - - let index : Int = Int((touchLocation.x + (xSection / 2)) / xSection) - if index >= 0 && index < dataSets.dataPoints.count { - locations.append(HashablePoint(x: CGFloat(index) * xSection, - y: (CGFloat(dataSets.dataPoints[index].value - minValue) * -ySection) + chartSize.size.height)) - } - return locations - } - - public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { - self.markerSubView(dataSet: self.dataSets, touchLocation: touchLocation, chartSize: chartSize) - } - - // MARK: - Legends +// MARK: - Legends +extension LineChartData: LegendProtocol { + internal func setupLegends() { if dataSets.style.colourType == .colour, @@ -214,32 +213,7 @@ public final class LineChartData: LineChartDataProtocol, LegendProtocol { } } - // MARK: - Data Functions - public func getRange() -> Double { - switch self.chartStyle.baseline { - case .minimumValue: - return DataFunctions.dataSetRange(from: dataSets) - case .minimumWithMaximum(of: let value): - return DataFunctions.dataSetMaxValue(from: dataSets) - min(DataFunctions.dataSetMinValue(from: dataSets), value) - case .zero: - return DataFunctions.dataSetMaxValue(from: dataSets) - } - } - public func getMinValue() -> Double { - switch self.chartStyle.baseline { - case .minimumValue: - return DataFunctions.dataSetMinValue(from: dataSets) - case .minimumWithMaximum(of: let value): - return min(DataFunctions.dataSetMinValue(from: dataSets), value) - case .zero: - return 0 - } - } - internal func legendOrder() -> [LegendData] { return legends.sorted { $0.prioity < $1.prioity} } - - public typealias Set = LineDataSet - public typealias DataPoint = LineChartDataPoint } diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index 6e460d50..e4c6862b 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -51,9 +51,9 @@ import SwiftUI } ``` */ -public final class MultiLineChartData: LineChartDataProtocol, LegendProtocol { - - // MARK: - Properties +public final class MultiLineChartData: LineChartDataProtocol { + + // MARK: Properties public let id : UUID = UUID() @Published public var dataSets : MultiLineDataSet @@ -68,7 +68,7 @@ public final class MultiLineChartData: LineChartDataProtocol, LegendProtocol { public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) - // MARK: - Initializers + // MARK: Initializers /// Initialises a Multi Line Chart. /// /// - Parameters: @@ -94,7 +94,7 @@ public final class MultiLineChartData: LineChartDataProtocol, LegendProtocol { self.setupLegends() } - // MARK: - Labels + // MARK: Labels public func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { @@ -139,57 +139,57 @@ public final class MultiLineChartData: LineChartDataProtocol, LegendProtocol { } } - // MARK: - Points + // MARK: Points public func getPointMarker() -> some View { ForEach(self.dataSets.dataSets, id: \.self) { dataSet in PointsSubView(dataSets : dataSet, - minValue : self.getMinValue(), - range : self.getRange(), + minValue : self.minValue, + range : self.range, animation : self.chartStyle.globalAnimation, isFilled : self.isFilled) } } - // MARK: - Touch - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [LineChartDataPoint] { - var points : [LineChartDataPoint] = [] - for dataSet in dataSets.dataSets { - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) - let index = Int((touchLocation.x + (xSection / 2)) / xSection) - if index >= 0 && index < dataSet.dataPoints.count { - points.append(dataSet.dataPoints[index]) + // MARK: Touch + public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + self.infoView.isTouchCurrent = true + self.infoView.touchLocation = touchLocation + self.infoView.chartSize = chartSize.frame(in: .local) + self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) + } + + public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { + ZStack { + ForEach(self.dataSets.dataSets, id: \.self) { dataSet in + self.markerSubView(dataSet: dataSet, touchLocation: touchLocation, chartSize: chartSize) } } - return points } - public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { + + public typealias Set = MultiLineDataSet + public typealias DataPoint = LineChartDataPoint + public typealias CTStyle = LineChartStyle + +} - var locations : [HashablePoint] = [] +// MARK: - Touch +extension MultiLineChartData: TouchProtocol { + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { + var points : [LineChartDataPoint] = [] for dataSet in dataSets.dataSets { - - let minValue : Double = self.getMinValue() - let range : Double = self.getRange() - - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) - let ySection : CGFloat = chartSize.size.height / CGFloat(range) - let index : Int = Int((touchLocation.x + (xSection / 2)) / xSection) + let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) + let index = Int((touchLocation.x + (xSection / 2)) / xSection) if index >= 0 && index < dataSet.dataPoints.count { - locations.append(HashablePoint(x: CGFloat(index) * xSection, - y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.size.height)) - } - } - return locations - } - - public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { - ZStack { - ForEach(self.dataSets.dataSets, id: \.self) { dataSet in - self.markerSubView(dataSet: dataSet, touchLocation: touchLocation, chartSize: chartSize) + points.append(dataSet.dataPoints[index]) } } + self.infoView.touchOverlayInfo = points } +} - // MARK: - Legends +// MARK: - Legends +extension MultiLineChartData: LegendProtocol { + internal func setupLegends() { for dataSet in dataSets.dataSets { if dataSet.style.colourType == .colour, @@ -229,34 +229,7 @@ public final class MultiLineChartData: LineChartDataProtocol, LegendProtocol { } } - // MARK: - Data Functions - public func getRange() -> Double { - switch self.chartStyle.baseline { - case .minimumValue: - return DataFunctions.multiDataSetRange(from: dataSets) - case .minimumWithMaximum(of: let value): - return DataFunctions.multiDataSetMaxValue(from: dataSets) - min(DataFunctions.multiDataSetMinValue(from: dataSets), value) - case .zero: - return DataFunctions.multiDataSetMaxValue(from: dataSets) - } - } - public func getMinValue() -> Double { - switch self.chartStyle.baseline { - case .minimumValue: - return DataFunctions.multiDataSetMinValue(from: dataSets) - case .minimumWithMaximum(of: let value): - return min(DataFunctions.multiDataSetMinValue(from: dataSets), value) - case .zero: - return 0 - } - } - internal func legendOrder() -> [LegendData] { return legends.sorted { $0.prioity < $1.prioity} } - - - public typealias Set = MultiLineDataSet - public typealias DataPoint = LineChartDataPoint } - diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index d6c274d6..db924ae5 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -35,17 +35,7 @@ public protocol LineChartDataProtocol: LineAndBarChartData { - Returns: The position to place the indicator. */ func getIndicatorLocation(rect: CGRect, dataPoints: [LineChartDataPoint], touchLocation: CGPoint, lineType: LineType) -> CGPoint - - /** - Gets the location of a data point within the view. - - Parameters: - - touchLocation: Current location of the touch. - - chartSize: The size of the chart view as the parent view. - - dataSet: The data set to search in. - - Returns: The location on screen of data points. - */ - func getSinglePoint(touchLocation: CGPoint, chartSize: GeometryProxy, dataSet: LineDataSet) -> CGPoint - + /// Displays a view contatining touch markers. /// - Parameters: /// - dataSet: The data set to search in. diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index 2ac4e166..df52a285 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -11,8 +11,8 @@ import SwiftUI extension LineAndBarChartData where Self: LineChartDataProtocol { public func getYLabels() -> [Double] { var labels : [Double] = [Double]() - let dataRange : Double = self.getRange() - let minValue : Double = self.getMinValue() + let dataRange : Double = self.minValue + let minValue : Double = self.range let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) labels.append(minValue) @@ -34,8 +34,8 @@ extension LineChartDataProtocol { let path = getPath(lineType : lineType, rect : rect, dataPoints : dataPoints, - minValue : self.getMinValue(), - range : self.getRange(), + minValue : self.minValue, + range : self.range, touchLocation: touchLocation, isFilled : false) @@ -254,9 +254,10 @@ extension LineChartDataProtocol { // MARK: - Markers extension LineChartDataProtocol { - public func getSinglePoint(touchLocation: CGPoint, chartSize: GeometryProxy, dataSet: LineDataSet) -> CGPoint { - let minValue : Double = self.getMinValue() - let range : Double = self.getRange() + public func getPointLocation(dataSet: LineDataSet, touchLocation: CGPoint, chartSize: GeometryProxy) -> CGPoint? { + + let minValue : Double = self.minValue + let range : Double = self.range let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) let ySection : CGFloat = chartSize.size.height / CGFloat(range) @@ -265,8 +266,9 @@ extension LineChartDataProtocol { return CGPoint(x: CGFloat(index) * xSection, y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.size.height) } - return .zero + return nil } + } extension LineChartDataProtocol where Self.CTLineAndBarCS.Mark == LineMarkerType { @ViewBuilder public func markerSubView(dataSet : LineDataSet, @@ -278,7 +280,7 @@ extension LineChartDataProtocol where Self.CTLineAndBarCS.Mark == LineMarkerType 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.frame(in: .global), @@ -302,10 +304,12 @@ extension LineChartDataProtocol where Self.CTLineAndBarCS.Mark == LineMarkerType IndicatorSwitch(indicator: indicator, location: position) case .point: - Vertical(position: self.getSinglePoint(touchLocation: touchLocation, - chartSize: chartSize, - dataSet: dataSet)) - .stroke(Color.primary, lineWidth: 2) + if let position = self.getPointLocation(dataSet: dataSet, + touchLocation: touchLocation, + chartSize: chartSize) { + Vertical(position: position) + .stroke(Color.primary, lineWidth: 2) + } } case .full(attachment: let attach): @@ -322,12 +326,16 @@ extension LineChartDataProtocol where Self.CTLineAndBarCS.Mark == LineMarkerType .stroke(Color.primary, lineWidth: 2) IndicatorSwitch(indicator: indicator, location: position) - + case .point: - MarkerFull(position: self.getSinglePoint(touchLocation: touchLocation, - chartSize: chartSize, - dataSet: dataSet)) - .stroke(Color.primary, lineWidth: 2) + + if let position = self.getPointLocation(dataSet: dataSet, + touchLocation: touchLocation, + chartSize: chartSize) { + + MarkerFull(position: position) + .stroke(Color.primary, lineWidth: 2) + } } case .bottomLeading(attachment: let attach): @@ -346,10 +354,14 @@ extension LineChartDataProtocol where Self.CTLineAndBarCS.Mark == LineMarkerType IndicatorSwitch(indicator: indicator, location: position) case .point: - MarkerBottomLeading(position: self.getSinglePoint(touchLocation: touchLocation, - chartSize: chartSize, - dataSet: dataSet)) - .stroke(Color.primary, lineWidth: 2) + + if let position = self.getPointLocation(dataSet: dataSet, + touchLocation: touchLocation, + chartSize: chartSize) { + + MarkerBottomLeading(position: position) + .stroke(Color.primary, lineWidth: 2) + } } case .bottomTrailing(attachment: let attach): @@ -368,10 +380,14 @@ extension LineChartDataProtocol where Self.CTLineAndBarCS.Mark == LineMarkerType IndicatorSwitch(indicator: indicator, location: position) case .point: - MarkerBottomTrailing(position: self.getSinglePoint(touchLocation: touchLocation, - chartSize: chartSize, - dataSet: dataSet)) - .stroke(Color.primary, lineWidth: 2) + + if let position = self.getPointLocation(dataSet: dataSet, + touchLocation: touchLocation, + chartSize: chartSize) { + + MarkerBottomTrailing(position: position) + .stroke(Color.primary, lineWidth: 2) + } } case .topLeading(attachment: let attach): @@ -390,10 +406,14 @@ extension LineChartDataProtocol where Self.CTLineAndBarCS.Mark == LineMarkerType IndicatorSwitch(indicator: indicator, location: position) case .point: - MarkerTopLeading(position: self.getSinglePoint(touchLocation: touchLocation, - chartSize: chartSize, - dataSet: dataSet)) - .stroke(Color.primary, lineWidth: 2) + + if let position = self.getPointLocation(dataSet: dataSet, + touchLocation: touchLocation, + chartSize: chartSize) { + + MarkerTopLeading(position: position) + .stroke(Color.primary, lineWidth: 2) + } } case .topTrailing(attachment: let attach): @@ -412,10 +432,14 @@ extension LineChartDataProtocol where Self.CTLineAndBarCS.Mark == LineMarkerType IndicatorSwitch(indicator: indicator, location: position) case .point: - MarkerTopTrailing(position: self.getSinglePoint(touchLocation: touchLocation, - chartSize: chartSize, - dataSet: dataSet)) - .stroke(Color.primary, lineWidth: 2) + + if let position = self.getPointLocation(dataSet: dataSet, + touchLocation: touchLocation, + chartSize: chartSize) { + + MarkerTopTrailing(position: position) + .stroke(Color.primary, lineWidth: 2) + } } } } diff --git a/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift index 7a32e738..e9a6bf61 100644 --- a/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift @@ -19,8 +19,8 @@ internal struct PointMarkers: ViewModifier where T: LineChartDataProtocol { internal init(chartData : T) { self.chartData = chartData - self.minValue = chartData.getMinValue() - self.range = chartData.getRange() + self.minValue = chartData.minValue + self.range = chartData.range } internal func body(content: Content) -> some View { ZStack { diff --git a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift index 32f655d9..16c73b06 100644 --- a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift @@ -51,8 +51,8 @@ public struct FilledLineChart: View where ChartData: LineChartData { /// - Parameter chartData: Must be LineChartData model. public init(chartData: ChartData) { self.chartData = chartData - self.minValue = chartData.getMinValue() - self.range = chartData.getRange() + self.minValue = chartData.minValue + self.range = chartData.range self.chartData.isFilled = true } diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index 3f190bd2..83c318fb 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -54,8 +54,8 @@ public struct LineChart: View where ChartData: LineChartData { /// - Parameter chartData: Must be LineChartData model. public init(chartData: ChartData) { self.chartData = chartData - self.minValue = chartData.getMinValue() - self.range = chartData.getRange() + self.minValue = chartData.minValue + self.range = chartData.range } public var body: some View { diff --git a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift index beaa330c..d57fdf00 100644 --- a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift @@ -51,8 +51,8 @@ public struct MultiLineChart: View where ChartData: MultiLineChartDat /// - Parameter chartData: Must be MultiLineChartData model. public init(chartData: ChartData) { self.chartData = chartData - self.minValue = chartData.getMinValue() - self.range = chartData.getRange() + self.minValue = chartData.minValue + self.range = chartData.range } @State private var startAnimation : Bool = false diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift index d45db552..91568a94 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift @@ -29,10 +29,10 @@ import SwiftUI } ``` */ -public final class DoughnutChartData: DoughnutChartDataProtocol, LegendProtocol { +public final class DoughnutChartData: DoughnutChartDataProtocol { + // MARK: Properties public var id : UUID = UUID() - @Published public var dataSets : PieDataSet @Published public var metadata : ChartMetadata @Published public var chartStyle : DoughnutChartStyle @@ -42,7 +42,7 @@ public final class DoughnutChartData: DoughnutChartDataProtocol, LegendProtocol public var noDataText: Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) - // MARK: - Initializer + // MARK: Initializer /// Initialises a Doughnut Chart. /// /// - Parameters: @@ -67,13 +67,40 @@ public final class DoughnutChartData: DoughnutChartDataProtocol, LegendProtocol self.makeDataPoints() } - public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } - - internal func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} + // MARK: Touch + public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + self.infoView.isTouchCurrent = true + self.infoView.touchLocation = touchLocation + self.infoView.chartSize = chartSize.frame(in: .local) + self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) } + public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } + public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint public typealias CTStyle = DoughnutChartStyle } + +// MARK: - Touch +extension DoughnutChartData: TouchProtocol { + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { + var points : [PieChartDataPoint] = [] + let touchDegree = degree(from: touchLocation, in: chartSize.frame(in: .local)) + + 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 { + points.append(data) + } + self.infoView.touchOverlayInfo = points + } +} + +// MARK: - Legends +extension DoughnutChartData: LegendProtocol { + func setupLegends() {} + + internal func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } +} diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift index 0eee08d7..56aed4a0 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift @@ -84,6 +84,7 @@ import SwiftUI */ public final class MultiLayerPieChartData: MultiPieChartDataProtocol { + // MARK: Properties public var id : UUID = UUID() @Published public var dataSets : MultiPieDataSet @Published public var metadata : ChartMetadata @@ -94,7 +95,7 @@ public final class MultiLayerPieChartData: MultiPieChartDataProtocol { public var noDataText: Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) - // MARK: - Initializer + // MARK: Initializer /// Initialises a multi layered pie chart. /// /// - Parameters: @@ -115,31 +116,38 @@ public final class MultiLayerPieChartData: MultiPieChartDataProtocol { self.noDataText = noDataText self.chartType = (chartType: .pie, dataSetType: .single) -// self.setupLegends() - + self.setupLegends() self.makeDataPoints() } - public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } - + // MARK: Touch + public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + self.infoView.isTouchCurrent = true + self.infoView.touchLocation = touchLocation + self.infoView.chartSize = chartSize.frame(in: .local) + self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) + } + public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [MultiPieDataPoint] { + public typealias Set = MultiPieDataSet + public typealias DataPoint = MultiPieDataPoint + public typealias CTStyle = PieChartStyle +} + +// MARK: - Touch +extension MultiLayerPieChartData: TouchProtocol { + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { let points : [MultiPieDataPoint] = [] - return points + self.infoView.touchOverlayInfo = points } - - public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - return [HashablePoint(x: touchLocation.x, y: touchLocation.y)] - } - +} + +// MARK: - Legends +extension MultiLayerPieChartData: LegendProtocol { internal func setupLegends() {} internal func legendOrder() -> [LegendData] { return legends.sorted { $0.prioity < $1.prioity} } - - public typealias Set = MultiPieDataSet - public typealias DataPoint = MultiPieDataPoint - public typealias CTStyle = PieChartStyle } diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift index 5b93e720..0903a617 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift @@ -29,8 +29,9 @@ import SwiftUI } ``` */ -public final class PieChartData: PieChartDataProtocol, LegendProtocol { +public final class PieChartData: PieChartDataProtocol { + // MARK: Properties public var id : UUID = UUID() @Published public var dataSets : PieDataSet @Published public var metadata : ChartMetadata @@ -41,7 +42,7 @@ public final class PieChartData: PieChartDataProtocol, LegendProtocol { public var noDataText: Text public var chartType: (chartType: ChartType, dataSetType: DataSetType) - // MARK: - Initializer + // MARK: Initializer /// Initialises a Pie Chart. /// /// - Parameters: @@ -67,14 +68,51 @@ public final class PieChartData: PieChartDataProtocol, LegendProtocol { self.makeDataPoints() } - public func touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } - - internal func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} + // MARK: Touch + public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + self.infoView.isTouchCurrent = true + self.infoView.touchLocation = touchLocation + self.infoView.chartSize = chartSize.frame(in: .local) + self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) } + public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } + public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint public typealias CTStyle = PieChartStyle } +// MARK: - Touch +extension PieChartData: TouchProtocol { + public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { + var points : [PieChartDataPoint] = [] + let touchDegree = degree(from: touchLocation, in: chartSize.frame(in: .local)) + + 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 { + points.append(data) + } + self.infoView.touchOverlayInfo = points + } +} + +// MARK: - Legends +extension PieChartData: LegendProtocol { + internal func setupLegends() { + for data in dataSets.dataPoints { + if let legend = data.pointDescription { + self.legends.append(LegendData(id : data.id, + legend : legend, + colour : data.colour, + strokeStyle: nil, + prioity : 1, + chartType : .pie)) + } + } + } + + internal func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } +} diff --git a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolExtentions.swift b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolExtentions.swift index 5a2882f6..9856ee1c 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolExtentions.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolExtentions.swift @@ -69,6 +69,12 @@ extension PieAndDoughnutChartDataProtocol where Set == MultiPieDataSet, DataPoin } } +extension PieAndDoughnutChartDataProtocol { + public func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: GeometryProxy) -> CGPoint? { + return nil + } +} + extension PieAndDoughnutChartDataProtocol where Set == PieDataSet, DataPoint == PieChartDataPoint { /** @@ -87,33 +93,6 @@ extension PieAndDoughnutChartDataProtocol where Set == PieDataSet, DataPoint == } } - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [PieChartDataPoint] { - var points : [PieChartDataPoint] = [] - let touchDegree = degree(from: touchLocation, in: chartSize.frame(in: .local)) - - 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 { - points.append(data) - } - return points - } - - public func getPointLocation(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] { - return [HashablePoint(x: touchLocation.x, y: touchLocation.y)] - } - - internal func setupLegends() { - for data in dataSets.dataPoints { - if let legend = data.pointDescription { - self.legends.append(LegendData(id : data.id, - legend : legend, - colour : data.colour, - strokeStyle: nil, - prioity : 1, - chartType : .pie)) - } - } - } /** Gets the number of degrees around the chart from 'north'. diff --git a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift index a80e22a8..f3e9df49 100644 --- a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift @@ -48,10 +48,10 @@ public struct InfoViewData { Used by `HeaderBox` and `InfoBox`. */ - var positionX : CGFloat = 0 + var touchLocation : CGPoint = .zero /** - Current width of the `Info Box`. + Size of the chart. Used to set the location of the data point readout View. @@ -59,7 +59,7 @@ public struct InfoViewData { Used by `HeaderBox` and `InfoBox`. */ - var frame : CGRect = .zero + var chartSize : CGRect = .zero /** Current width of the `YAxisLabels` diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 95d4a2a9..3e2825e5 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -18,10 +18,13 @@ public protocol ChartData: ObservableObject, Identifiable { /// A type representing a data set. -- `DataSet` associatedtype Set : DataSet + /// A type representing a data point. -- `CTChartDataPoint` associatedtype DataPoint: CTChartDataPoint + /// A type representing the chart style. -- `CTChartStyle` associatedtype CTStyle : CTChartStyle + /// A type representing opaque View associatedtype Touch : View @@ -74,9 +77,27 @@ public protocol ChartData: ObservableObject, Identifiable { Returns whether there are two or more data points. */ func isGreaterThanTwo() -> Bool - - + // MARK: Touch + func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) + /** + Takes touch location and return a view based on the chart type and configuration. + + - 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: GeometryProxy) -> Touch + +} + + + +// MARK: - Touch Protocol +internal protocol TouchProtocol { + /// A type representing a data set. -- `DataSet` + associatedtype SetPoint : DataSet /** Gets the nearest data points to the touch location. - Parameters: @@ -84,7 +105,7 @@ public protocol ChartData: ObservableObject, Identifiable { - chartSize: The size of the chart view as the parent view. - Returns: Array of data points. */ - func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) -> [DataPoint] + func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) /** Gets the location of the data point in the view. @@ -93,20 +114,11 @@ public protocol ChartData: ObservableObject, Identifiable { - 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(touchLocation: CGPoint, chartSize: GeometryProxy) -> [HashablePoint] - - /** - Takes touch location and return a view based on the chart type and configuration. - - - 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 touchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> Touch - - + func getPointLocation(dataSet: SetPoint, touchLocation: CGPoint, chartSize: GeometryProxy) -> CGPoint? } + + +// MARK: - Legend Protocol /** Protocol for dealing with legend data internally. */ diff --git a/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift b/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift index 38eb6792..5ceb9628 100644 --- a/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift +++ b/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift @@ -11,10 +11,6 @@ import SwiftUI internal struct Vertical: Shape { private var position : CGPoint - - @inlinable internal init(position : HashablePoint) { - self.position = CGPoint(x: position.x, y: position.y) - } @inlinable internal init(position : CGPoint) { self.position = position @@ -35,10 +31,6 @@ internal struct Vertical: Shape { internal struct MarkerFull: Shape { private var position : CGPoint - - @inlinable internal init(position : HashablePoint) { - self.position = CGPoint(x: position.x, y: position.y) - } @inlinable internal init(position : CGPoint) { self.position = position @@ -65,10 +57,6 @@ internal struct MarkerBottomLeading: Shape { private var position : CGPoint - @inlinable internal init(position : HashablePoint) { - self.position = CGPoint(x: position.x, y: position.y) - } - @inlinable internal init(position : CGPoint) { self.position = position } @@ -94,10 +82,6 @@ internal struct MarkerBottomTrailing: Shape { private var position : CGPoint - @inlinable internal init(position : HashablePoint) { - self.position = CGPoint(x: position.x, y: position.y) - } - @inlinable internal init(position : CGPoint) { self.position = position } @@ -123,10 +107,6 @@ internal struct MarkerTopLeading: Shape { private var position : CGPoint - @inlinable internal init(position : HashablePoint) { - self.position = CGPoint(x: position.x, y: position.y) - } - @inlinable internal init(position : CGPoint) { self.position = position } @@ -151,10 +131,6 @@ internal struct MarkerTopLeading: Shape { internal struct MarkerTopTrailing: Shape { private var position : CGPoint - - @inlinable internal init(position : HashablePoint) { - self.position = CGPoint(x: position.x, y: position.y) - } @inlinable internal init(position : CGPoint) { self.position = position diff --git a/Sources/SwiftUICharts/Shared/Types/HashablePoint.swift b/Sources/SwiftUICharts/Shared/Types/HashablePoint.swift deleted file mode 100644 index d42f1db9..00000000 --- a/Sources/SwiftUICharts/Shared/Types/HashablePoint.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// HashablePoint.swift -// -// -// Created by Will Dale on 03/02/2021. -// - -import SwiftUI - -/** - A hashable version of CGPoint - - CGPoint doesn't conform to Hashable. - */ -public struct HashablePoint: Hashable { - - public let x : CGFloat - public let y : CGFloat - - public init(x: CGFloat, y: CGFloat) { - self.x = x - self.y = y - } -} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift index 2c93071a..4aef164d 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift @@ -37,9 +37,9 @@ internal struct InfoBox: ViewModifier where T: ChartData { valueColour : chartData.chartStyle.infoBoxValueColour, descriptionColour: chartData.chartStyle.infoBoxDescriptionColour, boxFrame : $boxFrame) - .position(x: setBoxLocationation(touchLocation: chartData.infoView.positionX, + .position(x: setBoxLocationation(touchLocation: chartData.infoView.touchLocation.x, boxFrame : boxFrame, - chartSize : chartData.infoView.frame), + chartSize : chartData.infoView.chartSize), y: 15) .frame(height: 40) } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 175c18d5..98ad6696 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -22,11 +22,6 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { self.chartData.infoView.touchSpecifier = specifier } - /// Current location of the touch input - @State private var touchLocation : CGPoint = CGPoint(x: 0, y: 0) - /// Frame information of the data point information box - @State private var boxFrame : CGRect = CGRect(x: 0, y: 0, width: 0, height: 50) - internal func body(content: Content) -> some View { Group { if chartData.isGreaterThanTwo() { @@ -36,13 +31,10 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { .gesture( DragGesture(minimumDistance: 0) .onChanged { (value) in - touchLocation = value.location - - chartData.infoView.isTouchCurrent = true - chartData.infoView.touchOverlayInfo = chartData.getDataPoint(touchLocation: touchLocation, chartSize: geo) - chartData.infoView.positionX = setBoxLocationation(touchLocation: touchLocation, boxFrame: boxFrame, chartSize: geo).x - chartData.infoView.frame = geo.frame(in: .local) + chartData.setTouchInteraction(touchLocation: value.location, + chartSize: geo) + } .onEnded { _ in chartData.infoView.isTouchCurrent = false @@ -50,31 +42,14 @@ internal struct TouchOverlay: ViewModifier where T: ChartData { } ) if chartData.infoView.isTouchCurrent { - chartData.touchInteraction(touchLocation: touchLocation, chartSize: geo) + chartData.getTouchInteraction(touchLocation: chartData.infoView.touchLocation, + chartSize: geo) } } } } else { content } } } - // MOVE TO PROTOCOL -- SEE INFOBOX - /// Sets the 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: CGPoint, boxFrame: CGRect, chartSize: GeometryProxy) -> CGPoint { - - var returnPoint : CGPoint = .zero - - if touchLocation.x < chartSize.frame(in: .local).minX + (boxFrame.width / 2) { - returnPoint.x = chartSize.frame(in: .local).minX + (boxFrame.width / 2) - } else if touchLocation.x > chartSize.frame(in: .local).maxX - (boxFrame.width / 2) { - returnPoint.x = chartSize.frame(in: .local).maxX - (boxFrame.width / 2) - } else { - returnPoint.x = touchLocation.x - } - return returnPoint - } } #endif diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index e6dd944b..2feba4b0 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -18,6 +18,26 @@ public protocol LineAndBarChartData : ChartData { /// A type representing opaque View 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. */ @@ -47,33 +67,16 @@ public protocol LineAndBarChartData : ChartData { */ func getYLabels() -> [Double] - /** - Returns the difference between the highest and lowest numbers in the data set or data sets. - */ - func getRange() -> Double - - /** - Returns the lowest value in the data set or data sets. - */ - func getMinValue() -> Double - - /** - Returns the highest value in the data set or data sets - */ - func getMaxValue() -> Double - - /** - Returns the average value from the data set or data sets. - */ - func getAverage() -> 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. diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift index aaa702db..58a64d04 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift @@ -8,30 +8,76 @@ import Foundation extension LineAndBarChartData where Set: SingleDataSet { - public func getRange() -> Double { - DataFunctions.dataSetRange(from: dataSets) + public var range : Double { + return DataFunctions.dataSetRange(from: dataSets) } - public func getMinValue() -> Double { - DataFunctions.dataSetMinValue(from: dataSets) + public var minValue : Double { + return DataFunctions.dataSetMinValue(from: dataSets) } - public func getMaxValue() -> Double { - DataFunctions.dataSetMaxValue(from: dataSets) + public var maxValue : Double { + return DataFunctions.dataSetMaxValue(from: dataSets) } - public func getAverage() -> Double { - DataFunctions.dataSetAverage(from: dataSets) + public var average : Double { + return DataFunctions.dataSetAverage(from: dataSets) } } + extension LineAndBarChartData where Set: MultiDataSet { - public func getRange() -> Double { - DataFunctions.multiDataSetRange(from: dataSets) + public var range : Double { + return DataFunctions.multiDataSetRange(from: dataSets) + } + public var minValue : Double { + return DataFunctions.multiDataSetMinValue(from: dataSets) + } + public var maxValue : Double { + return DataFunctions.multiDataSetMaxValue(from: dataSets) + } + public var average : Double { + return DataFunctions.multiDataSetAverage(from: dataSets) } - public func getMinValue() -> Double { - DataFunctions.multiDataSetMinValue(from: dataSets) +} + +extension LineAndBarChartData where Self: LineChartData { + public var range : Double { + switch self.chartStyle.baseline { + case .minimumValue: + return DataFunctions.dataSetRange(from: dataSets) + case .minimumWithMaximum(of: let value): + return DataFunctions.dataSetMaxValue(from: dataSets) - min(DataFunctions.dataSetMinValue(from: dataSets), value) + case .zero: + return DataFunctions.dataSetMaxValue(from: dataSets) + } } - public func getMaxValue() -> Double { - DataFunctions.multiDataSetMaxValue(from: dataSets) + public var minValue : Double { + switch self.chartStyle.baseline { + case .minimumValue: + return DataFunctions.dataSetMinValue(from: dataSets) + case .minimumWithMaximum(of: let value): + return min(DataFunctions.dataSetMinValue(from: dataSets), value) + case .zero: + return 0 + } + } +} +extension LineAndBarChartData where Self: MultiLineChartData { + public var range : Double { + switch self.chartStyle.baseline { + case .minimumValue: + return DataFunctions.multiDataSetRange(from: dataSets) + case .minimumWithMaximum(of: let value): + return DataFunctions.multiDataSetMaxValue(from: dataSets) - min(DataFunctions.multiDataSetMinValue(from: dataSets), value) + case .zero: + return DataFunctions.multiDataSetMaxValue(from: dataSets) + } } - public func getAverage() -> Double { - DataFunctions.multiDataSetAverage(from: dataSets) + public var minValue : Double { + switch self.chartStyle.baseline { + case .minimumValue: + return DataFunctions.multiDataSetMinValue(from: dataSets) + case .minimumWithMaximum(of: let value): + return min(DataFunctions.multiDataSetMinValue(from: dataSets), value) + case .zero: + return 0 + } } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift index 302f01b6..710e18e3 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift @@ -23,19 +23,19 @@ internal struct XAxisLabels: ViewModifier where T: LineAndBarChartData { Group { switch chartData.chartStyle.xAxisLabelPosition { case .top: - if chartData.isGreaterThanTwo() { +// if chartData.isGreaterThanTwo() { VStack { chartData.getXAxisLabels() content } - } else { content } +// } else { content } case .bottom: - if chartData.isGreaterThanTwo() { +// if chartData.isGreaterThanTwo() { VStack { content chartData.getXAxisLabels() } - } else { content } +// } else { content } } } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index 306b2f56..87b46d22 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -48,10 +48,11 @@ internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { self.labelColour = labelColour self.labelBackground = labelBackground - self.markerValue = isAverage ? chartData.getAverage() : markerValue - self.maxValue = chartData.getMaxValue() - self.range = chartData.getRange() - self.minValue = chartData.getMinValue() + self.markerValue = isAverage ? chartData.average : markerValue + self.maxValue = chartData.maxValue + self.range = chartData.range + self.minValue = chartData.minValue + } @State private var startAnimation : Bool = false From dadf7807995109e3c57d4e17d3d46921d38b34bb Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 24 Feb 2021 16:56:26 +0000 Subject: [PATCH 088/152] Rename protocols. --- .../Models/ChartData/BarChartData.swift | 12 +++++-- .../ChartData/GroupedBarChartData.swift | 11 ++++++- .../ChartData/StackedBarChartData.swift | 11 ++++++- .../Models/DataSet/MultiBarDataSets.swift | 2 +- .../Models/Protocols/BarChartProtocols.swift | 31 ++++++++++--------- .../BarChartProtocolsExtensions.swift | 20 ------------ .../Shapes/RoundedRectangleBarShape.swift | 2 +- .../Models/ChartData/LineChartData.swift | 15 ++++++++- .../Models/ChartData/MultiLineChartData.swift | 15 ++++++++- .../Models/DataSet/MultiLineDataSet.swift | 2 +- .../LineChart/Models/LineChartDataPoint.swift | 2 +- .../Models/Protocols/LineChartProtocols.swift | 14 ++++----- .../LineChartProtocolsExtensions.swift | 30 +++++------------- .../ViewModifiers/PointMarkers.swift | 8 ++--- .../Views/SubViews/LineChartSubViews.swift | 6 ++-- .../Models/ChartData/DoughnutChartData.swift | 2 +- .../ChartData/MultiLayerPieChartData.swift | 2 +- .../Models/ChartData/PieChartData.swift | 2 +- .../Models/DataSets/MultiPieDataSet.swift | 2 +- .../PieChart/Models/DataSets/PieDataSet.swift | 2 +- .../Models/Protocols/PieChartProtocols.swift | 18 +++++------ ...wift => PieChartProtocolsExtentions.swift} | 6 ++-- .../Shared/Extras/DataFunctions.swift | 16 +++++----- .../Shared/Extras/Extensions.swift | 1 - .../Models/Protocols/SharedProtocols.swift | 20 ++++++------ .../Protocols/SharedProtocolsExtensions.swift | 4 +-- .../Shared/ViewModifiers/HeaderBox.swift | 4 +-- .../Shared/ViewModifiers/InfoBox.swift | 4 +-- .../Shared/ViewModifiers/Legends.swift | 4 +-- .../Shared/ViewModifiers/TouchOverlay.swift | 6 ++-- .../Shared/Views/CustomNoDataView.swift | 2 +- .../Shared/Views/LegendView.swift | 2 +- .../Protocols/LineAndBarProtocols.swift | 17 +++------- .../LineAndBarProtocolsExtentions.swift | 8 ++--- .../ViewModifiers/AxisBorders.swift | 10 +++--- .../ViewModifiers/XAxisGrid.swift | 6 ++-- .../ViewModifiers/XAxisLabels.swift | 6 ++-- .../ViewModifiers/YAxisGrid.swift | 6 ++-- .../ViewModifiers/YAxisLabels.swift | 6 ++-- .../ViewModifiers/YAxisPOI.swift | 10 +++--- .../Views/HorizontalGridView.swift | 2 +- .../Views/VerticalGridView.swift | 2 +- 42 files changed, 182 insertions(+), 169 deletions(-) delete mode 100644 Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift rename Sources/SwiftUICharts/PieChart/Models/Protocols/{PieChartProtocolExtentions.swift => PieChartProtocolsExtentions.swift} (95%) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 6192f852..aa488dc9 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -40,7 +40,7 @@ import SwiftUI } ``` */ -public final class BarChartData: BarChartDataProtocol { +public final class BarChartData: CTBarChartDataProtocol { // MARK: Properties public let id : UUID = UUID() @@ -54,7 +54,7 @@ public final class BarChartData: BarChartDataProtocol { @Published public var infoView : InfoViewData = InfoViewData() public var noDataText : Text - public var chartType : (chartType: ChartType, dataSetType: DataSetType) + public let chartType : (chartType: ChartType, dataSetType: DataSetType) // MARK: Initializer /// Initialises a standard Bar Chart. @@ -126,6 +126,14 @@ public final class BarChartData: BarChartDataProtocol { } } } + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let maxValue: Double = self.maxValue + for index in 0...self.chartStyle.yAxisNumberOfLabels { + labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) + } + return labels + } // MARK: Touch public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 81c4ab30..30b7a330 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -76,7 +76,7 @@ import SwiftUI } ``` */ -public final class GroupedBarChartData: MultiBarChartDataProtocol { +public final class GroupedBarChartData: CTMultiBarChartDataProtocol { // MARK: Properties public let id : UUID = UUID() @@ -172,6 +172,15 @@ public final class GroupedBarChartData: MultiBarChartDataProtocol { } } + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let maxValue: Double = self.maxValue + for index in 0...self.chartStyle.yAxisNumberOfLabels { + labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) + } + return labels + } + // MARK: Touch public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { self.infoView.isTouchCurrent = true diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index e97a911f..a34dd659 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -72,7 +72,7 @@ import SwiftUI chartStyle: BarChartStyle(xAxisLabelsFrom: .dataPoint)) ``` */ -public final class StackedBarChartData: MultiBarChartDataProtocol { +public final class StackedBarChartData: CTMultiBarChartDataProtocol { // MARK: Properties public let id : UUID = UUID() @@ -158,6 +158,15 @@ public final class StackedBarChartData: MultiBarChartDataProtocol { } } + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let maxValue: Double = self.maxValue + for index in 0...self.chartStyle.yAxisNumberOfLabels { + labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) + } + return labels + } + // MARK: Touch public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { self.infoView.isTouchCurrent = true diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift index 98cb3b57..6c22c1be 100644 --- a/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift +++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift @@ -22,7 +22,7 @@ import SwiftUI ]) ``` */ -public struct MultiBarDataSets: MultiDataSet { +public struct MultiBarDataSets: CTMultiDataSetProtocol { public let id : UUID = UUID() public var dataSets : [MultiBarDataSet] diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index a88fe963..878435eb 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -9,9 +9,9 @@ import SwiftUI // MARK: - Chart Data /** - A protocol to extend functionality of `LineAndBarChartData` specifically for Bar Charts. + A protocol to extend functionality of `CTLineBarChartDataProtocol` specifically for Bar Charts. */ -public protocol BarChartDataProtocol: LineAndBarChartData { +public protocol CTBarChartDataProtocol: CTLineBarChartDataProtocol { /** Overall styling for the bars @@ -20,9 +20,9 @@ public protocol BarChartDataProtocol: LineAndBarChartData { } /** - A protocol to extend functionality of `BarChartDataProtocol` specifically for Multi Part Bar Charts. + A protocol to extend functionality of `CTBarChartDataProtocol` specifically for Multi Part Bar Charts. */ -public protocol MultiBarChartDataProtocol: BarChartDataProtocol { +public protocol CTMultiBarChartDataProtocol: CTBarChartDataProtocol { /** Grouping data to inform the chart about the relationship between the datapoints. @@ -36,9 +36,9 @@ public protocol MultiBarChartDataProtocol: BarChartDataProtocol { // MARK: - Style /** - A protocol to extend functionality of `CTLineAndBarChartStyle` specifically for Bar Charts. + A protocol to extend functionality of `CTLineBarChartStyle` specifically for Bar Charts. */ -public protocol CTBarChartStyle: CTLineAndBarChartStyle {} +public protocol CTBarChartStyle: CTLineBarChartStyle {} @@ -49,9 +49,9 @@ public protocol CTBarChartStyle: CTLineAndBarChartStyle {} // MARK: - DataSet /** - A protocol to extend functionality of `SingleDataSet` specifically for Standard Bar Charts. + A protocol to extend functionality of `CTSingleDataSetProtocol` specifically for Standard Bar Charts. */ -public protocol CTStandardBarChartDataSet: SingleDataSet { +public protocol CTStandardBarChartDataSet: CTSingleDataSetProtocol { /** Label to display in the legend. */ @@ -59,9 +59,9 @@ public protocol CTStandardBarChartDataSet: SingleDataSet { } /** - A protocol to extend functionality of `SingleDataSet` specifically for Multi Part Bar Charts. + A protocol to extend functionality of `CTSingleDataSetProtocol` specifically for Multi Part Bar Charts. */ -public protocol CTMultiBarChartDataSet: SingleDataSet {} +public protocol CTMultiBarChartDataSet: CTSingleDataSetProtocol {} @@ -74,21 +74,24 @@ public protocol CTMultiBarChartDataSet: SingleDataSet {} // MARK: - DataPoints /** - A protocol to extend functionality of `CTLineAndBarDataPoint` specifically for standard Bar Charts. + A protocol to extend functionality of `CTLineBarDataPoint` specifically for standard Bar Charts. */ -public protocol CTBarDataPoint: CTLineAndBarDataPoint {} +public protocol CTBarDataPoint: CTLineBarDataPoint {} /** - A protocol to extend functionality of `CTLineAndBarDataPoint` specifically for standard Bar Charts. + A protocol to extend functionality of `CTLineBarDataPoint` specifically for standard Bar Charts. */ public protocol CTStandardBarDataPoint: CTBarDataPoint, CTColourStyle {} /** - A protocol to extend functionality of `CTLineAndBarDataPoint` specifically for multi part Bar Charts. + A protocol to extend functionality of `CTLineBarDataPoint` specifically for multi part Bar Charts. i.e: Grouped or Stacked */ public protocol CTMultiBarDataPoint: CTBarDataPoint { + /** + 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 deleted file mode 100644 index 66d59859..00000000 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// BarChartProtocolsExtensions.swift -// -// -// Created by Will Dale on 19/02/2021. -// - -import SwiftUI - -// Standard / Grouped / Stacked -extension LineAndBarChartData where Self: BarChartDataProtocol { - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let maxValue: Double = self.maxValue - for index in 0...self.chartStyle.yAxisNumberOfLabels { - labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) - } - return labels - } -} diff --git a/Sources/SwiftUICharts/BarChart/Shapes/RoundedRectangleBarShape.swift b/Sources/SwiftUICharts/BarChart/Shapes/RoundedRectangleBarShape.swift index 9c350e4e..d1d5db95 100644 --- a/Sources/SwiftUICharts/BarChart/Shapes/RoundedRectangleBarShape.swift +++ b/Sources/SwiftUICharts/BarChart/Shapes/RoundedRectangleBarShape.swift @@ -10,7 +10,7 @@ import SwiftUI /** Round rectange used for the bar shapes - [Reference](https://stackoverflow.com/a/56763282) + [SO](https://stackoverflow.com/a/56763282) */ internal struct RoundedRectangleBarShape: Shape { diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index c911fa0d..a13ff9fc 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -44,7 +44,7 @@ import SwiftUI ``` */ -public final class LineChartData: LineChartDataProtocol { +public final class LineChartData: CTLineChartDataProtocol { // MARK: Properties public let id : UUID = UUID() @@ -131,6 +131,19 @@ public final class LineChartData: LineChartDataProtocol { } } } + + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let dataRange : Double = self.minValue + let minValue : Double = self.range + let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) + + labels.append(minValue) + for index in 1...self.chartStyle.yAxisNumberOfLabels { + labels.append(minValue + range * Double(index)) + } + return labels + } // MARK: Points public func getPointMarker() -> some View { diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index e4c6862b..bf1fa83d 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -51,7 +51,7 @@ import SwiftUI } ``` */ -public final class MultiLineChartData: LineChartDataProtocol { +public final class MultiLineChartData: CTLineChartDataProtocol { // MARK: Properties public let id : UUID = UUID() @@ -139,6 +139,19 @@ public final class MultiLineChartData: LineChartDataProtocol { } } + public func getYLabels() -> [Double] { + var labels : [Double] = [Double]() + let dataRange : Double = self.minValue + let minValue : Double = self.range + let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) + + labels.append(minValue) + for index in 1...self.chartStyle.yAxisNumberOfLabels { + labels.append(minValue + range * Double(index)) + } + return labels + } + // MARK: Points public func getPointMarker() -> some View { ForEach(self.dataSets.dataSets, id: \.self) { dataSet in diff --git a/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift index 078514da..a2747826 100644 --- a/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift @@ -42,7 +42,7 @@ MultiLineDataSet(dataSets: [ ]) ``` */ -public struct MultiLineDataSet: MultiDataSet { +public struct MultiLineDataSet: CTMultiDataSetProtocol { public let id : UUID = UUID() public var dataSets : [LineDataSet] diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift index e45e3b5d..f735c362 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift @@ -18,7 +18,7 @@ import SwiftUI date : Date()) ``` */ -public struct LineChartDataPoint: CTLineAndBarDataPoint { +public struct LineChartDataPoint: CTLineBarDataPoint { public let id : UUID = UUID() diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index db924ae5..c2342545 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -9,9 +9,9 @@ import SwiftUI // MARK: - Chart Data /** - A protocol to extend functionality of `LineAndBarChartData` specifically for Line Charts. + A protocol to extend functionality of `CTLineBarChartDataProtocol` specifically for Line Charts. */ -public protocol LineChartDataProtocol: LineAndBarChartData { +public protocol CTLineChartDataProtocol: CTLineBarChartDataProtocol { /// A type representing opaque View associatedtype Marker : View @@ -52,9 +52,9 @@ public protocol LineChartDataProtocol: LineAndBarChartData { // MARK: - Style /** - A protocol to extend functionality of `CTLineAndBarChartStyle` specifically for Line Charts. + A protocol to extend functionality of `CTLineBarChartStyle` specifically for Line Charts. */ -public protocol CTLineChartStyle : CTLineAndBarChartStyle { +public protocol CTLineChartStyle : CTLineBarChartStyle { /** Where to start drawing the line chart from. Zero or data set minium. */ @@ -67,7 +67,7 @@ public protocol CTLineChartStyle : CTLineAndBarChartStyle { /** A protocol to extend functionality of `SingleDataSet` specifically for Line Charts. */ -public protocol CTLineChartDataSet: SingleDataSet { +public protocol CTLineChartDataSet: CTSingleDataSetProtocol { /// A type representing colour styling associatedtype Styling : CTColourStyle @@ -80,7 +80,7 @@ public protocol CTLineChartDataSet: SingleDataSet { /** Sets the style for the Data Set (as opposed to Chart Data Style). */ - var style : Styling { get set } + var style : Styling { get set } /** Sets the look of the markers over the data points. @@ -88,5 +88,5 @@ public protocol CTLineChartDataSet: SingleDataSet { The markers are layed out when the ViewModifier `PointMarkers` is applied. */ - var pointStyle : PointStyle { get set } + var pointStyle : PointStyle { get set } } diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index df52a285..e89db6bd 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -7,24 +7,8 @@ import SwiftUI -// Standard / Multi -extension LineAndBarChartData where Self: LineChartDataProtocol { - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let dataRange : Double = self.minValue - let minValue : Double = self.range - let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) - - labels.append(minValue) - for index in 1...self.chartStyle.yAxisNumberOfLabels { - labels.append(minValue + range * Double(index)) - } - return labels - } -} - // MARK: - Position Indicator -extension LineChartDataProtocol { +extension CTLineChartDataProtocol { public func getIndicatorLocation(rect: CGRect, dataPoints: [LineChartDataPoint], touchLocation: CGPoint, @@ -90,7 +74,7 @@ extension LineChartDataProtocol { The total length of the path. # Reference - https://developer.apple.com/documentation/swiftui/path/element + [Apple](https://developer.apple.com/documentation/swiftui/path/element) - Parameter path: Path to measure. - Returns: Total length of the path. @@ -227,7 +211,7 @@ extension LineChartDataProtocol { Returns a point on the path based on the X axis of the users touch input. # Reference - https://swiftui-lab.com/swiftui-animations-part2/ + [SwiftUI Lab](https://swiftui-lab.com/swiftui-animations-part2/) - Parameters: - percent: The distance along the path as a percentage. @@ -252,7 +236,7 @@ extension LineChartDataProtocol { } // MARK: - Markers -extension LineChartDataProtocol { +extension CTLineChartDataProtocol { public func getPointLocation(dataSet: LineDataSet, touchLocation: CGPoint, chartSize: GeometryProxy) -> CGPoint? { @@ -270,7 +254,7 @@ extension LineChartDataProtocol { } } -extension LineChartDataProtocol where Self.CTLineAndBarCS.Mark == LineMarkerType { +extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { @ViewBuilder public func markerSubView(dataSet : LineDataSet, touchLocation : CGPoint, chartSize : GeometryProxy @@ -281,7 +265,9 @@ extension LineChartDataProtocol where Self.CTLineAndBarCS.Mark == LineMarkerType EmptyView() case .indicator(let style): - PosistionIndicator(fillColour: style.fillColour, lineColour: style.lineColour, lineWidth: style.lineWidth) + PosistionIndicator(fillColour: style.fillColour, + lineColour: style.lineColour, + lineWidth: style.lineWidth) .frame(width: style.size, height: style.size) .position(self.getIndicatorLocation(rect: chartSize.frame(in: .global), dataPoints: dataSet.dataPoints, diff --git a/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift index e9a6bf61..aac9b3fc 100644 --- a/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift @@ -10,7 +10,7 @@ import SwiftUI /** ViewModifier for for laying out point markers. */ -internal struct PointMarkers: ViewModifier where T: LineChartDataProtocol { +internal struct PointMarkers: ViewModifier where T: CTLineChartDataProtocol { @ObservedObject var chartData: T @@ -36,10 +36,10 @@ 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 + The style of the markers is set in the PointStyle data model as parameter in CTChartData - Requires: - Chart Data to conform to LineChartDataProtocol. + Chart Data to conform to CTLineChartDataProtocol. - LineChartData - MultiLineChartData @@ -59,7 +59,7 @@ extension View { - Returns: A new view containing the chart with point markers. */ - public func pointMarkers(chartData: T) -> some View { + public func pointMarkers(chartData: T) -> some View { self.modifier(PointMarkers(chartData: chartData)) } } diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift index 4394ea1c..cd67cd53 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift @@ -13,7 +13,7 @@ import SwiftUI Single colour */ -internal struct LineChartColourSubView: View where CD: LineChartDataProtocol { +internal struct LineChartColourSubView: View where CD: CTLineChartDataProtocol { private let chartData : CD private let dataSet : LineDataSet @@ -71,7 +71,7 @@ internal struct LineChartColourSubView: View where CD: LineChartDataProtocol Gradient colour */ -internal struct LineChartColoursSubView: View where CD: LineChartDataProtocol { +internal struct LineChartColoursSubView: View where CD: CTLineChartDataProtocol { private let chartData : CD private let dataSet : LineDataSet @@ -146,7 +146,7 @@ internal struct LineChartColoursSubView: View where CD: LineChartDataProtoco Gradient with stops */ -internal struct LineChartStopsSubView: View where CD: LineChartDataProtocol { +internal struct LineChartStopsSubView: View where CD: CTLineChartDataProtocol { private let chartData : CD private let dataSet : LineDataSet diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift index 91568a94..3750f1a9 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift @@ -29,7 +29,7 @@ import SwiftUI } ``` */ -public final class DoughnutChartData: DoughnutChartDataProtocol { +public final class DoughnutChartData: CTDoughnutChartDataProtocol { // MARK: Properties public var id : UUID = UUID() diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift index 56aed4a0..854d4c81 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift @@ -82,7 +82,7 @@ import SwiftUI } ``` */ -public final class MultiLayerPieChartData: MultiPieChartDataProtocol { +public final class MultiLayerPieChartData: CTMultiPieChartDataProtocol { // MARK: Properties public var id : UUID = UUID() diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift index 0903a617..865b6881 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift @@ -29,7 +29,7 @@ import SwiftUI } ``` */ -public final class PieChartData: PieChartDataProtocol { +public final class PieChartData: CTPieChartDataProtocol { // MARK: Properties public var id : UUID = UUID() diff --git a/Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift b/Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift index f9699826..1578b888 100644 --- a/Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift +++ b/Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift @@ -24,7 +24,7 @@ import SwiftUI ]) ``` */ -public struct MultiPieDataSet: SingleDataSet { +public struct MultiPieDataSet: CTSingleDataSetProtocol { public var id: UUID = UUID() public var dataPoints : [MultiPieDataPoint] diff --git a/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift b/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift index 8a7531b9..f272232b 100644 --- a/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift +++ b/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift @@ -21,7 +21,7 @@ import SwiftUI legendTitle: "Data") ``` */ -public struct PieDataSet: SingleDataSet { +public struct PieDataSet: CTSingleDataSetProtocol { public var id : UUID = UUID() public var dataPoints : [PieChartDataPoint] diff --git a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift index d0fa2c85..b2cb860d 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift @@ -9,30 +9,30 @@ import SwiftUI // MARK: - Chart Data /** - A protocol to extend functionality of `ChartData` specifically for Pie and Doughnut Charts. + A protocol to extend functionality of `CTChartData` specifically for Pie and Doughnut Charts. */ -public protocol PieAndDoughnutChartDataProtocol: ChartData {} +public protocol CTPieDoughnutChartDataProtocol: CTChartData {} /** - A protocol to extend functionality of `PieAndDoughnutChartDataProtocol` specifically for Pie Charts. + A protocol to extend functionality of `CTPieDoughnutChartDataProtocol` specifically for Pie Charts. */ -public protocol PieChartDataProtocol : PieAndDoughnutChartDataProtocol {} +public protocol CTPieChartDataProtocol : CTPieDoughnutChartDataProtocol {} /** - A protocol to extend functionality of `PieAndDoughnutChartDataProtocol` specifically for Doughnut Charts. + A protocol to extend functionality of `CTPieDoughnutChartDataProtocol` specifically for Doughnut Charts. */ -public protocol DoughnutChartDataProtocol : PieAndDoughnutChartDataProtocol {} +public protocol CTDoughnutChartDataProtocol : CTPieDoughnutChartDataProtocol {} /** - A protocol to extend functionality of `PieAndDoughnutChartDataProtocol` specifically for multi layer Pie Charts. + A protocol to extend functionality of `CTPieDoughnutChartDataProtocol` specifically for multi layer Pie Charts. */ -public protocol MultiPieChartDataProtocol : PieAndDoughnutChartDataProtocol {} +public protocol CTMultiPieChartDataProtocol : CTPieDoughnutChartDataProtocol {} // MARK: - DataSet -public protocol CTMultiPieDataSet: DataSet {} +public protocol CTMultiPieDataSet: CTDataSetProtocol {} diff --git a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolExtentions.swift b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift similarity index 95% rename from Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolExtentions.swift rename to Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift index 9856ee1c..975f7b94 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolExtentions.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift @@ -8,7 +8,7 @@ import SwiftUI // MARK: - Extentions -extension PieAndDoughnutChartDataProtocol where Set == MultiPieDataSet, DataPoint == MultiPieDataPoint { +extension CTPieDoughnutChartDataProtocol where Set == MultiPieDataSet, DataPoint == MultiPieDataPoint { /** Sets up the data points in a way that can be sent to renderer for drawing. @@ -69,13 +69,13 @@ extension PieAndDoughnutChartDataProtocol where Set == MultiPieDataSet, DataPoin } } -extension PieAndDoughnutChartDataProtocol { +extension CTPieDoughnutChartDataProtocol { public func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: GeometryProxy) -> CGPoint? { return nil } } -extension PieAndDoughnutChartDataProtocol where Set == PieDataSet, DataPoint == PieChartDataPoint { +extension CTPieDoughnutChartDataProtocol where Set == PieDataSet, DataPoint == PieChartDataPoint { /** Sets up the data points in a way that can be sent to renderer for drawing. diff --git a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift index e5aefc09..6e5c58e2 100644 --- a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift +++ b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift @@ -18,7 +18,7 @@ import Foundation - Parameter dataSet: Target data set. - Returns: Highest value in data set. */ - static func dataSetMaxValue(from dataSet: T) -> Double { + static func dataSetMaxValue(from dataSet: T) -> Double { return dataSet.dataPoints.max { $0.value < $1.value }?.value ?? 0 } @@ -27,7 +27,7 @@ import Foundation - Parameter dataSet: Target data set. - Returns: Lowest value in data set. */ - static func dataSetMinValue(from dataSet: T) -> Double { + static func dataSetMinValue(from dataSet: T) -> Double { return dataSet.dataPoints.min { $0.value < $1.value }?.value ?? 0 } @@ -36,7 +36,7 @@ import Foundation - Parameter dataSet: Target data set. - Returns: Average of values in data set. */ - static func dataSetAverage(from dataSet: T) -> Double { + static func dataSetAverage(from dataSet: T) -> Double { let sum = dataSet.dataPoints.reduce(0) { $0 + $1.value } return sum / Double(dataSet.dataPoints.count) } @@ -46,7 +46,7 @@ import Foundation - Parameter dataSet: Target data set. - Returns: Difference between the highest and lowest values in data set. */ - static func dataSetRange(from dataSet: T) -> Double { + static func dataSetRange(from dataSet: T) -> Double { let maxValue = dataSet.dataPoints.max { $0.value < $1.value }?.value ?? 0 let minValue = dataSet.dataPoints.min { $0.value < $1.value }?.value ?? 0 @@ -64,7 +64,7 @@ import Foundation - Parameter dataSet: Target data sets. - Returns: Highest value in data sets. */ - static func multiDataSetMaxValue(from dataSets: T) -> Double { + static func multiDataSetMaxValue(from dataSets: T) -> Double { var setHolder : [Double] = [] for set in dataSets.dataSets { setHolder.append(set.dataPoints.max { $0.value < $1.value }?.value ?? 0) @@ -77,7 +77,7 @@ import Foundation - Parameter dataSet: Target data sets. - Returns: Lowest value in data sets. */ - static func multiDataSetMinValue(from dataSets: T) -> Double { + static func multiDataSetMinValue(from dataSets: T) -> Double { var setHolder : [Double] = [] for set in dataSets.dataSets { setHolder.append(set.dataPoints.min { $0.value < $1.value }?.value ?? 0) @@ -90,7 +90,7 @@ import Foundation - Parameter dataSet: Target data sets. - Returns: Average of values in data sets. */ - static func multiDataSetAverage(from dataSets: T) -> Double { + static func multiDataSetAverage(from dataSets: T) -> Double { var setHolder : [Double] = [] for set in dataSets.dataSets { let sum = set.dataPoints.reduce(0) { $0 + $1.value } @@ -105,7 +105,7 @@ import Foundation - Parameter dataSet: Target data sets. - Returns: Difference between the highest and lowest values in data sets. */ - static func multiDataSetRange(from dataSets: T) -> Double { + static func multiDataSetRange(from dataSets: T) -> Double { var setMaxHolder : [Double] = [] for set in dataSets.dataSets { setMaxHolder.append(set.dataPoints.max { $0.value < $1.value }?.value ?? 0) diff --git a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift index cd9191f0..441be8e3 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift @@ -11,7 +11,6 @@ extension View { /** View modifier to conditionally add a view modifier. - # Reference [SO](https://stackoverflow.com/a/62962375) */ @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Transform) -> some View { diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 3e2825e5..80b5413a 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -14,10 +14,10 @@ import SwiftUI All Chart Data models ultimately conform to this. */ -public protocol ChartData: ObservableObject, Identifiable { +public protocol CTChartData: ObservableObject, Identifiable { - /// A type representing a data set. -- `DataSet` - associatedtype Set : DataSet + /// A type representing a data set. -- `CTDataSetProtocol` + associatedtype Set : CTDataSetProtocol /// A type representing a data point. -- `CTChartDataPoint` associatedtype DataPoint: CTChartDataPoint @@ -96,8 +96,10 @@ public protocol ChartData: ObservableObject, Identifiable { // MARK: - Touch Protocol internal protocol TouchProtocol { - /// A type representing a data set. -- `DataSet` - associatedtype SetPoint : DataSet + + /// A type representing a data set. -- `CTDataSetProtocol` + associatedtype SetPoint : CTDataSetProtocol + /** Gets the nearest data points to the touch location. - Parameters: @@ -143,14 +145,14 @@ internal protocol LegendProtocol { /** Main protocol to set conformace for types of Data Sets. */ -public protocol DataSet: Hashable, Identifiable { +public protocol CTDataSetProtocol: Hashable, Identifiable { var id : ID { get } } /** Protocol for data sets that only require a single set of data . */ -public protocol SingleDataSet: DataSet { +public protocol CTSingleDataSetProtocol: CTDataSetProtocol { /// A type representing a data point. -- `CTChartDataPoint` associatedtype DataPoint : CTChartDataPoint @@ -164,9 +166,9 @@ public protocol SingleDataSet: DataSet { /** Protocol for data sets that require a multiple sets of data . */ -public protocol MultiDataSet: DataSet { +public protocol CTMultiDataSetProtocol: CTDataSetProtocol { /// A type representing a single data set -- `SingleDataSet` - associatedtype DataSet : SingleDataSet + associatedtype DataSet : CTSingleDataSetProtocol /** Array of single data sets. diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift index f900ecfc..1a025a5e 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift @@ -7,13 +7,13 @@ import Foundation -extension ChartData where Set: SingleDataSet { +extension CTChartData where Set: CTSingleDataSetProtocol { public func isGreaterThanTwo() -> Bool { return dataSets.dataPoints.count > 2 } } -extension ChartData where Set: MultiDataSet { +extension CTChartData where Set: CTMultiDataSetProtocol { public func isGreaterThanTwo() -> Bool { var returnValue: Bool = true dataSets.dataSets.forEach { dataSet in diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index 797d9446..515b4337 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -10,7 +10,7 @@ import SwiftUI /** Displays the metadata about the chart as well as optionally touch overlay information. */ -internal struct HeaderBox: ViewModifier where T: ChartData { +internal struct HeaderBox: ViewModifier where T: CTChartData { @ObservedObject var chartData: T @@ -111,7 +111,7 @@ extension View { - Tag: HeaderBox */ - public func headerBox(chartData: T) -> some View { + 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 index 4aef164d..fa6217a9 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift @@ -10,7 +10,7 @@ import SwiftUI /** A view that displays information from `TouchOverlay`. */ -internal struct InfoBox: ViewModifier where T: ChartData { +internal struct InfoBox: ViewModifier where T: CTChartData { @ObservedObject var chartData: T @@ -105,7 +105,7 @@ extension View { - Returns: A new view containing the chart with a view to display touch overlay information. */ - public func infoBox(chartData: T) -> some View { + 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 fe0c203a..976f584f 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift @@ -10,7 +10,7 @@ import SwiftUI /** Displays legends under the chart. */ -internal struct Legends: ViewModifier where T: ChartData { +internal struct Legends: ViewModifier where T: CTChartData { @ObservedObject var chartData: T private let columns : [GridItem] @@ -47,7 +47,7 @@ extension View { - 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 { + 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/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 98ad6696..64ea7eac 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -11,7 +11,7 @@ import SwiftUI /** Finds the nearest data point and displays the relevent information. */ -internal struct TouchOverlay: ViewModifier where T: ChartData { +internal struct TouchOverlay: ViewModifier where T: CTChartData { @ObservedObject var chartData: T @@ -75,7 +75,7 @@ extension View { - specifier: Decimal precision for labels. - Returns: A new view containing the chart with a touch overlay. */ - public func touchOverlay(chartData: T, + public func touchOverlay(chartData: T, specifier: String = "%.0f" ) -> some View { self.modifier(TouchOverlay(chartData: chartData, @@ -88,7 +88,7 @@ extension View { - Attention: Unavailable in tvOS */ - public func touchOverlay(chartData: T, + public func touchOverlay(chartData: T, specifier: String = "%.0f" ) -> some View { self.modifier(EmptyModifier()) diff --git a/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift b/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift index d9fc6958..bfae4c4f 100644 --- a/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift +++ b/Sources/SwiftUICharts/Shared/Views/CustomNoDataView.swift @@ -10,7 +10,7 @@ import SwiftUI /** View to display text if there is not enough data to draw the chart. */ -public struct CustomNoDataView: View where T: ChartData { +public struct CustomNoDataView: View where T: CTChartData { let chartData : T diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index 34769703..6c6b3e08 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -10,7 +10,7 @@ import SwiftUI /** Sub view to setup and display the legends. */ -internal struct LegendView: View where T: ChartData { +internal struct LegendView: View where T: CTChartData { @ObservedObject var chartData : T private let columns : [GridItem] diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index 2feba4b0..a37b42d3 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -9,12 +9,10 @@ import SwiftUI // MARK: - Chart Data /** - A protocol to extend functionality of `ChartData` specifically for Line and Bar Charts. + A protocol to extend functionality of `CTChartData` specifically for Line and Bar Charts. */ -public protocol LineAndBarChartData : ChartData { +public protocol CTLineBarChartDataProtocol : CTChartData where CTStyle: CTLineBarChartStyle { - /// A type representing the chart style. -- `CTChartStyle` - associatedtype CTLineAndBarCS : CTLineAndBarChartStyle /// A type representing opaque View associatedtype XLabels : View @@ -52,11 +50,6 @@ public protocol LineAndBarChartData : ChartData { var viewData: ChartViewData { get set } - /** - Data model conatining the style data for the chart. - */ - var chartStyle: CTLineAndBarCS { get set } - /** Labels to display on the Y axis @@ -75,8 +68,6 @@ public protocol LineAndBarChartData : ChartData { } - - // MARK: - Style /** A protocol to get the correct touch overlay marker. @@ -86,7 +77,7 @@ public protocol MarkerType {} /** A protocol to extend functionality of `CTChartStyle` specifically for Line and Bar Charts. */ -public protocol CTLineAndBarChartStyle: CTChartStyle { +public protocol CTLineBarChartStyle: CTChartStyle { /// A type representing touch overlay marker type. -- `MarkerType` associatedtype Mark : MarkerType @@ -143,7 +134,7 @@ public protocol CTLineAndBarChartStyle: CTChartStyle { /** A protocol to extend functionality of `CTChartDataPoint` specifically for Line and Bar Charts. */ -public protocol CTLineAndBarDataPoint: CTChartDataPoint { +public protocol CTLineBarDataPoint: CTChartDataPoint { /** Data points label for the X axis. diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift index 58a64d04..80d496e5 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift @@ -7,7 +7,7 @@ import Foundation -extension LineAndBarChartData where Set: SingleDataSet { +extension CTLineBarChartDataProtocol where Set: CTSingleDataSetProtocol { public var range : Double { return DataFunctions.dataSetRange(from: dataSets) } @@ -22,7 +22,7 @@ extension LineAndBarChartData where Set: SingleDataSet { } } -extension LineAndBarChartData where Set: MultiDataSet { +extension CTLineBarChartDataProtocol where Set: CTMultiDataSetProtocol { public var range : Double { return DataFunctions.multiDataSetRange(from: dataSets) } @@ -37,7 +37,7 @@ extension LineAndBarChartData where Set: MultiDataSet { } } -extension LineAndBarChartData where Self: LineChartData { +extension CTLineBarChartDataProtocol where Self: LineChartData { public var range : Double { switch self.chartStyle.baseline { case .minimumValue: @@ -59,7 +59,7 @@ extension LineAndBarChartData where Self: LineChartData { } } } -extension LineAndBarChartData where Self: MultiLineChartData { +extension CTLineBarChartDataProtocol where Self: MultiLineChartData { public var range : Double { switch self.chartStyle.baseline { case .minimumValue: diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift index a9a997f1..b385041e 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/AxisBorders.swift @@ -10,7 +10,7 @@ import SwiftUI /** Dividing line drawn between the X axis labels and the chart. */ -internal struct XAxisBorder: ViewModifier where T: LineAndBarChartData { +internal struct XAxisBorder: ViewModifier where T: CTLineBarChartDataProtocol { @ObservedObject var chartData: T private let labelsAndTop : Bool @@ -51,13 +51,13 @@ internal struct XAxisBorder: ViewModifier where T: LineAndBarChartData { /** Dividing line drawn between the Y axis labels and the chart. */ -internal struct YAxisBorder: ViewModifier where T: LineAndBarChartData { +internal struct YAxisBorder: ViewModifier where T: CTLineBarChartDataProtocol { @ObservedObject var chartData: T private let labelsAndLeading : Bool private let labelsAndTrailing: Bool - init(chartData: T) { + 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 @@ -87,11 +87,11 @@ internal struct YAxisBorder: ViewModifier where T: LineAndBarChartData { } extension View { - internal func xAxisBorder(chartData: T) -> some View { + internal func xAxisBorder(chartData: T) -> some View { self.modifier(XAxisBorder(chartData: chartData)) } - internal func yAxisBorder(chartData: T) -> some View { + 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 index 5e6de77b..621781e7 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift @@ -10,7 +10,7 @@ import SwiftUI /** Adds vertical lines along the X axis. */ -internal struct XAxisGrid: ViewModifier where T: LineAndBarChartData { +internal struct XAxisGrid: ViewModifier where T: CTLineBarChartDataProtocol { @ObservedObject var chartData : T @@ -41,7 +41,7 @@ extension View { The style is set in ChartData --> LineChartStyle --> xAxisGridStyle - Requires: - Chart Data to conform to LineAndBarChartData. + Chart Data to conform to CTLineBarChartDataProtocol. # Available for: - Line Chart @@ -58,7 +58,7 @@ extension View { - Parameter chartData: Chart data model. - Returns: A new view containing the chart with vertical lines under it. */ - public func xAxisGrid(chartData: T) -> some View { + 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 index 710e18e3..618b49fc 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift @@ -10,7 +10,7 @@ import SwiftUI /** Labels for the X axis. */ -internal struct XAxisLabels: ViewModifier where T: LineAndBarChartData { +internal struct XAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol { @ObservedObject var chartData: T @@ -49,7 +49,7 @@ extension View { or ChartData --> DataSets --> DataPoints - Requires: - Chart Data to conform to LineAndBarChartData. + Chart Data to conform to CTLineBarChartDataProtocol. # Available for: - Line Chart @@ -66,7 +66,7 @@ extension View { - 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 { + 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 index 9de9e0b0..6868c6f7 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift @@ -10,7 +10,7 @@ import SwiftUI /** Adds horizontal lines along the X axis. */ -internal struct YAxisGrid: ViewModifier where T: LineAndBarChartData { +internal struct YAxisGrid: ViewModifier where T: CTLineBarChartDataProtocol { @ObservedObject var chartData : T @@ -40,7 +40,7 @@ extension View { The style is set in ChartData --> LineChartStyle --> yAxisGridStyle - Requires: - Chart Data to conform to LineAndBarChartData. + Chart Data to conform to CTLineBarChartDataProtocol. # Available for: - Line Chart @@ -57,7 +57,7 @@ extension View { - Parameter chartData: Chart data model. - Returns: A new view containing the chart with horizontal lines under it. */ - public func yAxisGrid(chartData: T) -> some View { + 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 index cd8061b3..ae1b835d 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift @@ -10,7 +10,7 @@ import SwiftUI /** Automatically generated labels for the Y axis. */ -internal struct YAxisLabels: ViewModifier where T: LineAndBarChartData { +internal struct YAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol { @ObservedObject var chartData: T @@ -99,7 +99,7 @@ extension View { Controls are in ChartData --> ChartStyle - Requires: - Chart Data to conform to LineAndBarChartData. + Chart Data to conform to CTLineBarChartDataProtocol. # Available for: - Line Chart @@ -117,7 +117,7 @@ extension View { - specifier: Decimal precision specifier - Returns: HStack of labels */ - public func yAxisLabels(chartData: T, specifier: String = "%.0f") -> some View { + 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 index 87b46d22..93a7e67b 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -10,7 +10,7 @@ import SwiftUI /** Configurable Point of interest */ -internal struct YAxisPOI: ViewModifier where T: LineAndBarChartData { +internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { @ObservedObject var chartData: T @@ -177,7 +177,7 @@ extension View { ``` - Requires: - Chart Data to conform to LineAndBarChartData. + Chart Data to conform to CTLineBarChartDataProtocol. # Available for: - Line Chart @@ -202,7 +202,7 @@ extension View { - strokeStyle: Style of Stroke. - Returns: A new view containing the chart with a marker line at a specified value. */ - public func yAxisPOI(chartData : T, + public func yAxisPOI(chartData : T, markerName : String, markerValue : Double, labelPosition : DisplayValue = .center(specifier: "%.0f"), @@ -246,7 +246,7 @@ extension View { ``` - Requires: - Chart Data to conform to LineAndBarChartData. + Chart Data to conform to CTLineBarChartDataProtocol. # Available for: - Line Chart @@ -272,7 +272,7 @@ extension View { - Tag: AverageLine */ - public func averageLine(chartData : T, + public func averageLine(chartData : T, markerName : String = "Average", labelPosition : DisplayValue = .yAxis(specifier: "%.0f"), labelColour : Color = Color.primary, diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift index f7b3b70e..9cca9cf2 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/HorizontalGridView.swift @@ -10,7 +10,7 @@ import SwiftUI /** Sub view of the Y axis grid view modifier. */ -internal struct HorizontalGridView: View where T: LineAndBarChartData { +internal struct HorizontalGridView: View where T: CTLineBarChartDataProtocol { @ObservedObject private var chartData : T diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift index fe2f7694..33b50037 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/VerticalGridView.swift @@ -10,7 +10,7 @@ import SwiftUI /** Sub view of the X axis grid view modifier. */ -internal struct VerticalGridView: View where T: LineAndBarChartData { +internal struct VerticalGridView: View where T: CTLineBarChartDataProtocol { @ObservedObject private var chartData : T From 358c690d837e68d0c21b9f2df00d8dc1ec314ea2 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 24 Feb 2021 21:26:01 +0000 Subject: [PATCH 089/152] Bug Fix for Y labels. --- .../SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift | 2 +- .../LineChart/Models/ChartData/LineChartData.swift | 5 ++--- .../LineChart/Models/ChartData/MultiLineChartData.swift | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift index ab85337c..6191316c 100644 --- a/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift +++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/BarDataSet.swift @@ -35,7 +35,7 @@ public struct BarDataSet: CTStandardBarChartDataSet { /// - dataPoints: Array of elements. /// - legendTitle: label for the data in legend. public init(dataPoints : [BarChartDataPoint], - legendTitle : String + legendTitle : String = "" ) { self.dataPoints = dataPoints self.legendTitle = legendTitle diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index a13ff9fc..be15c15e 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -134,10 +134,9 @@ public final class LineChartData: CTLineChartDataProtocol { public func getYLabels() -> [Double] { var labels : [Double] = [Double]() - let dataRange : Double = self.minValue - let minValue : Double = self.range + let dataRange : Double = self.range + let minValue : Double = self.minValue let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) - labels.append(minValue) for index in 1...self.chartStyle.yAxisNumberOfLabels { labels.append(minValue + range * Double(index)) diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index bf1fa83d..e37be6d3 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -141,8 +141,8 @@ public final class MultiLineChartData: CTLineChartDataProtocol { public func getYLabels() -> [Double] { var labels : [Double] = [Double]() - let dataRange : Double = self.minValue - let minValue : Double = self.range + let dataRange : Double = self.range + let minValue : Double = self.minValue let range : Double = dataRange / Double(self.chartStyle.yAxisNumberOfLabels) labels.append(minValue) From 49213a29c9a23d790ab73a0f6d63ce4e7e0fc8b2 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 24 Feb 2021 21:26:18 +0000 Subject: [PATCH 090/152] Tidy. --- .../Protocols/LineChartProtocolsExtensions.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index e89db6bd..ddbfa25d 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -127,8 +127,8 @@ extension CTLineChartDataProtocol { case .line(to: let nextPoint): if touchLocation.x < nextPoint.x { total += distanceToTouch(from : currentPoint, - to : nextPoint, - touchX: touchLocation.x) + to : nextPoint, + touchX: touchLocation.x) isComplete = true return } else { @@ -138,8 +138,8 @@ extension CTLineChartDataProtocol { case .curve(to: let nextPoint, control1: _, control2: _ ): if touchLocation.x < nextPoint.x { total += distanceToTouch(from : currentPoint, - to : nextPoint, - touchX: touchLocation.x) + to : nextPoint, + touchX: touchLocation.x) isComplete = true return } else { @@ -149,8 +149,8 @@ extension CTLineChartDataProtocol { case .quadCurve(to: let nextPoint, control: _): if touchLocation.x < nextPoint.x { total += distanceToTouch(from : currentPoint, - to : nextPoint, - touchX: touchLocation.x) + to : nextPoint, + touchX: touchLocation.x) isComplete = true return } else { From 52a08f757532e27968ccd273084b087781ded176 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 24 Feb 2021 21:26:40 +0000 Subject: [PATCH 091/152] Add more Unit Tests. --- ...4224B4FE-4B1F-4DC7-9D83-A7B7F9824962.plist | 22 ++ .../SwiftUIChartsTests.xcbaseline/Info.plist | 40 +++ Tests/LinuxMain.swift | 9 +- .../BarCharts/BarChartTests.swift | 79 ++++++ .../BarCharts/GroupedBarChartTests.swift | 132 +++++++++ .../BarCharts/StackedBarChartTests.swift | 132 +++++++++ .../LineCharts/LineChartPathTests.swift | 88 ++++++ .../LineCharts/LineChartTests.swift | 79 ++++++ .../LineCharts/MultiLineChartTest.swift | 131 +++++++++ .../SwiftUIChartsTests.swift | 256 ------------------ .../SwiftUIChartsTests/XCTestManifests.swift | 8 +- 11 files changed, 717 insertions(+), 259 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcbaselines/SwiftUIChartsTests.xcbaseline/4224B4FE-4B1F-4DC7-9D83-A7B7F9824962.plist create mode 100644 .swiftpm/xcode/xcshareddata/xcbaselines/SwiftUIChartsTests.xcbaseline/Info.plist create mode 100644 Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift create mode 100644 Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift create mode 100644 Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift create mode 100644 Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift create mode 100644 Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift create mode 100644 Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift delete mode 100644 Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift 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/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..76686b9f --- /dev/null +++ b/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift @@ -0,0 +1,79 @@ +import XCTest +@testable import SwiftUICharts + +final class BarChartTests: XCTestCase { + + 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) + } + + // MARK: Greater + 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 dataPoints = [ + BarChartDataPoint(value: 10), + BarChartDataPoint(value: 50), + BarChartDataPoint(value: 40), + BarChartDataPoint(value: 80) + ] + let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints), + chartStyle: BarChartStyle(yAxisNumberOfLabels: 3)) + + XCTAssertEqual(chartData.getYLabels()[0], 0.00000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[1], 26.6666, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[2], 53.3333, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[3], 80.0000, accuracy: 0.01) + } + + + + static var allTests = [ + // Data + ("testBarMaxValue", testBarMaxValue), + ("testBarMinValue", testBarMinValue), + ("testBarAverage", testBarAverage), + ("testBarRange", testBarRange), + // Greater + ("testBarIsGreaterThanTwoTrue", testBarIsGreaterThanTwoTrue), + ("testBarIsGreaterThanTwoFalse", testBarIsGreaterThanTwoFalse), + // Labels + ("testBarGetYLabels", testBarGetYLabels), + + + ] +} diff --git a/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift new file mode 100644 index 00000000..9aad1c80 --- /dev/null +++ b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift @@ -0,0 +1,132 @@ +import XCTest +@testable import SwiftUICharts + +final class GroupedBarChartTests: XCTestCase { + + 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: 50, 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: 20, xAxisLabel: "2.1", pointLabel: "Two One" , group: Group.one.data), + MultiBarChartDataPoint(value: 60, 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: 30, xAxisLabel: "3.1", pointLabel: "Three One" , group: Group.one.data), + MultiBarChartDataPoint(value: 70, 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: 90, xAxisLabel: "3.4", pointLabel: "Three Four" , group: Group.four.data) + ]), + + MultiBarDataSet(dataPoints: [ + MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", pointLabel: "Four One" , group: Group.one.data), + MultiBarChartDataPoint(value: 80, 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) + ]) + ]) + + // 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, xAxisLabel: "1.1", pointLabel: "One One" , group: Group.one.data) + ]), + + MultiBarDataSet(dataPoints: [ + MultiBarChartDataPoint(value: 20, xAxisLabel: "2.1", pointLabel: "Two One" , group: Group.one.data) + ]), + + MultiBarDataSet(dataPoints: [ + MultiBarChartDataPoint(value: 30, xAxisLabel: "3.1", pointLabel: "Three One", group: Group.one.data) + + ]), + + MultiBarDataSet(dataPoints: [ + MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", pointLabel: "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)) + + XCTAssertEqual(chartData.getYLabels()[0], 0.00000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[1], 30.0000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[2], 60.0000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[3], 90.0000, accuracy: 0.01) + + } + + static var allTests = [ + // Data + ("testGroupedBarMaxValue", testGroupedBarMaxValue), + ("testGroupedBarMinValue", testGroupedBarMinValue), + ("testGroupedBarAverage", testGroupedBarAverage), + ("testGroupedBarRange", testGroupedBarRange), + // Greater + ("testGroupedBarIsGreaterThanTwoTrue", testGroupedBarIsGreaterThanTwoTrue), + ("testGroupedBarIsGreaterThanTwoFalse", testGroupedBarIsGreaterThanTwoFalse), + // Labels + ("testGroupedBarGetYLabels", testGroupedBarGetYLabels), + + + ] +} diff --git a/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift new file mode 100644 index 00000000..e44b43ab --- /dev/null +++ b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift @@ -0,0 +1,132 @@ +import XCTest +@testable import SwiftUICharts + +final class StackedBarChartTests: XCTestCase { + + 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: 50, 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: 20, xAxisLabel: "2.1", pointLabel: "Two One" , group: Group.one.data), + MultiBarChartDataPoint(value: 60, 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: 30, xAxisLabel: "3.1", pointLabel: "Three One" , group: Group.one.data), + MultiBarChartDataPoint(value: 70, 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: 90, xAxisLabel: "3.4", pointLabel: "Three Four" , group: Group.four.data) + ]), + + MultiBarDataSet(dataPoints: [ + MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", pointLabel: "Four One" , group: Group.one.data), + MultiBarChartDataPoint(value: 80, 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) + ]) + ]) + + // 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, xAxisLabel: "1.1", pointLabel: "One One" , group: Group.one.data) + ]), + + MultiBarDataSet(dataPoints: [ + MultiBarChartDataPoint(value: 20, xAxisLabel: "2.1", pointLabel: "Two One" , group: Group.one.data) + ]), + + MultiBarDataSet(dataPoints: [ + MultiBarChartDataPoint(value: 30, xAxisLabel: "3.1", pointLabel: "Three One", group: Group.one.data) + + ]), + + MultiBarDataSet(dataPoints: [ + MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", pointLabel: "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)) + + XCTAssertEqual(chartData.getYLabels()[0], 0.00000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[1], 30.0000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[2], 60.0000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[3], 90.0000, accuracy: 0.01) + + } + + static var allTests = [ + // Data + ("testStackedBarMaxValue", testStackedBarMaxValue), + ("testStackedBarMinValue", testStackedBarMinValue), + ("testStackedBarAverage", testStackedBarAverage), + ("testStackedBarRange", testStackedBarRange), + // Greater + ("testStackedBarIsGreaterThanTwoTrue", testStackedBarIsGreaterThanTwoTrue), + ("testStackedBarIsGreaterThanTwoFalse", testStackedBarIsGreaterThanTwoFalse), + // Labels + ("testStackedBarGetYLabels", testStackedBarGetYLabels), + + + ] +} diff --git a/Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift b/Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift new file mode 100644 index 00000000..c1060e97 --- /dev/null +++ b/Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift @@ -0,0 +1,88 @@ +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 = chartData.getIndicatorLocation(rect: rect, + dataPoints: chartData.dataSets.dataPoints, + touchLocation: touchLocation, + lineType: .line) + + 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 = chartData.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 = chartData.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 = chartData.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 = chartData.relativePoint(from: pointOne, to: pointTwo, touchX: touchLocation.x) + + XCTAssertEqual(test.x, 25, accuracy: 0.01) + XCTAssertEqual(test.y, 25, accuracy: 0.01) + } + + + static var allTests = [ + ("testGetIndicatorLocation", testGetIndicatorLocation), + ("testGetPercentageOfPath", testGetPercentageOfPath), + ("testGetTotalLength", testGetTotalLength), + ("testGetLengthToTouch", testGetLengthToTouch), + ("testRelativePoint", testRelativePoint), + ] +} diff --git a/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift b/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift new file mode 100644 index 00000000..3e3bf653 --- /dev/null +++ b/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift @@ -0,0 +1,79 @@ +import XCTest +@testable import SwiftUICharts + +final class LineChartTests: XCTestCase { + + let dataPoints = [ + LineChartDataPoint(value: 10), + LineChartDataPoint(value: 40), + LineChartDataPoint(value: 30), + LineChartDataPoint(value: 60) + ] + + // MARK: - Data + func testLineMaxValue() { + let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) + XCTAssertEqual(chartData.maxValue, 60) + } + 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, 35) + } + func testLineRange() { + let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) + XCTAssertEqual(chartData.range, 50.001) + } + + // MARK: Greater + 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 dataPoints = [ + LineChartDataPoint(value: 10), + LineChartDataPoint(value: 50), + LineChartDataPoint(value: 40), + LineChartDataPoint(value: 80) + ] + let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints), + chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, + baseline: .minimumValue)) + + XCTAssertEqual(chartData.getYLabels()[0], 10.0000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[1], 33.3333, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[2], 56.6666, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[3], 80.0000, accuracy: 0.01) + + } + + + static var allTests = [ + // Data + ("testLineMaxValue", testLineMaxValue), + ("testLineMinValue", testLineMinValue), + ("testLineAverage", testLineAverage), + ("testLineRange", testLineRange), + // Greater + ("testLineIsGreaterThanTwoTrue", testLineIsGreaterThanTwoTrue), + ("testLineIsGreaterThanTwoFalse", testLineIsGreaterThanTwoFalse), + // Labels + ("testLineGetYLabels", testLineGetYLabels), + ] +} diff --git a/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift b/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift new file mode 100644 index 00000000..13c01667 --- /dev/null +++ b/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift @@ -0,0 +1,131 @@ +import XCTest +@testable import SwiftUICharts + +final class MultiLineChartTest: XCTestCase { + + // MARK: - Data + func testMultiLineMaxValue() { + + let chartData = MultiLineChartData(dataSets: + 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) + ]) + ])) + + XCTAssertEqual(chartData.maxValue, 100) + } + func testMultiLineMinValue() { + let chartData = MultiLineChartData(dataSets: + 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) + ]) + ])) + + XCTAssertEqual(chartData.minValue, 10) + } + func testMultiLineAverage() { + let chartData = MultiLineChartData(dataSets: + 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) + ]) + ])) + + XCTAssertEqual(chartData.average, 53.75) + } + func testMultiLineRange() { + let chartData = MultiLineChartData(dataSets: + 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) + ]) + ])) + + XCTAssertEqual(chartData.range, 90.001) + } + // MARK: Greater + func testMultiIsGreaterThanTwoTrue() { + let chartData = MultiLineChartData(dataSets: + 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) + ]) + ])) + + XCTAssertTrue(chartData.isGreaterThanTwo()) + } + + func testMultiIsGreaterThanTwoFalse() { + let chartData = MultiLineChartData(dataSets: + MultiLineDataSet(dataSets: [ + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 10), + ]), + LineDataSet(dataPoints: [ + LineChartDataPoint(value: 50) + ]) + ])) + + XCTAssertFalse(chartData.isGreaterThanTwo()) + } + + + + static var allTests = [ + // Data + ("testMultiLineMaxValue", testMultiLineMaxValue), + ("testMultiLineMinValue", testMultiLineMinValue), + ("testMultiLineAverage", testMultiLineAverage), + ("testMultiLineRange", testMultiLineRange), + // Greater + ("testMultiLineIsGreaterThanTwoTrue", testMultiIsGreaterThanTwoTrue), + ("testMultiLineIsGreaterThanTwoFalse", testMultiIsGreaterThanTwoFalse), + ] +} diff --git a/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift b/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift deleted file mode 100644 index 4e1159e1..00000000 --- a/Tests/SwiftUIChartsTests/SwiftUIChartsTests.swift +++ /dev/null @@ -1,256 +0,0 @@ -import XCTest -@testable import SwiftUICharts - -final class SwiftUIChartsTests: XCTestCase { - - // MARK: - Single Line Data - func testMaxValue() { - let dataPoints = [ - LineChartDataPoint(value: 10), - LineChartDataPoint(value: 40), - LineChartDataPoint(value: 30), - LineChartDataPoint(value: 60) - ] - let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) - - XCTAssertEqual(chartData.getMaxValue(), 60) - } - func testMinValue() { - let dataPoints = [ - LineChartDataPoint(value: 10), - LineChartDataPoint(value: 40), - LineChartDataPoint(value: 30), - LineChartDataPoint(value: 60) - ] - let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) - - XCTAssertEqual(chartData.getMinValue(), 10) - } - func testAverage() { - let dataPoints = [ - LineChartDataPoint(value: 10), - LineChartDataPoint(value: 40), - LineChartDataPoint(value: 30), - LineChartDataPoint(value: 60) - ] - let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) - - XCTAssertEqual(chartData.getAverage(), 35) - } - func testRange() { - let dataPoints = [ - LineChartDataPoint(value: 10), - LineChartDataPoint(value: 40), - LineChartDataPoint(value: 30), - LineChartDataPoint(value: 60) - ] - let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) - - XCTAssertEqual(chartData.getRange(), 50.001) - } - - - - // MARK: - Multi Line Data - func testMultiLineMaxValue() { - - let chartData = MultiLineChartData(dataSets: - MultiLineDataSet(dataSets: [ - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 10), - LineChartDataPoint(value: 40), - LineChartDataPoint(value: 30), - LineChartDataPoint(value: 60) - ], - legendTitle: "Bob"), - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 50), - LineChartDataPoint(value: 60), - LineChartDataPoint(value: 80), - LineChartDataPoint(value: 100) - ], - legendTitle: "Bob") - ])) - - XCTAssertEqual(chartData.getMaxValue(), 100) - } - func testMultiLineMinValue() { - let chartData = MultiLineChartData(dataSets: - MultiLineDataSet(dataSets: [ - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 10), - LineChartDataPoint(value: 40), - LineChartDataPoint(value: 30), - LineChartDataPoint(value: 60) - ], - legendTitle: "Bob"), - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 50), - LineChartDataPoint(value: 60), - LineChartDataPoint(value: 80), - LineChartDataPoint(value: 100) - ], - legendTitle: "Bob") - ])) - - XCTAssertEqual(chartData.getMinValue(), 10) - } - func testMultiLineAverage() { - let chartData = MultiLineChartData(dataSets: - MultiLineDataSet(dataSets: [ - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 10), - LineChartDataPoint(value: 40), - LineChartDataPoint(value: 30), - LineChartDataPoint(value: 60) - ], - legendTitle: "Bob"), - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 50), - LineChartDataPoint(value: 60), - LineChartDataPoint(value: 80), - LineChartDataPoint(value: 100) - ], - legendTitle: "Bob") - ])) - - XCTAssertEqual(chartData.getAverage(), 53.75) - } - func testMultiLineRange() { - let chartData = MultiLineChartData(dataSets: - MultiLineDataSet(dataSets: [ - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 10), - LineChartDataPoint(value: 40), - LineChartDataPoint(value: 30), - LineChartDataPoint(value: 60) - ], - legendTitle: "Bob"), - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 50), - LineChartDataPoint(value: 60), - LineChartDataPoint(value: 80), - LineChartDataPoint(value: 100) - ], - legendTitle: "Bob") - ])) - - XCTAssertEqual(chartData.getRange(), 90.001) - } - // MARK: - Labels - func testLineGetYLabels() { - let dataPoints = [ - LineChartDataPoint(value: 10), - LineChartDataPoint(value: 50), - LineChartDataPoint(value: 40), - LineChartDataPoint(value: 80) - ] - let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints), - chartStyle: LineChartStyle(yAxisNumberOfLabels: 3)) - - XCTAssertEqual(chartData.getYLabels()[0], 10.0000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[1], 33.3333, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[2], 56.6666, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[3], 80.0000, accuracy: 0.01) - - } - func testBarGetYLabels() { - let dataPoints = [ - BarChartDataPoint(value: 10), - BarChartDataPoint(value: 50), - BarChartDataPoint(value: 40), - BarChartDataPoint(value: 80) - ] - let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints, legendTitle: "Test"), - chartStyle: BarChartStyle(yAxisNumberOfLabels: 3)) - - XCTAssertEqual(chartData.getYLabels()[0], 0.00000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[1], 26.6666, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[2], 53.3333, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[3], 80.0000, accuracy: 0.01) - - } - - // MARK: - Chart Data - func testIsGreaterThanTwoTrue() { - let dataPoints = [ - LineChartDataPoint(value: 10), - LineChartDataPoint(value: 40), - LineChartDataPoint(value: 30), - LineChartDataPoint(value: 60) - ] - let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) - - XCTAssertTrue(chartData.isGreaterThanTwo()) - } - - func testIsGreaterThanTwoFalse() { - let dataPoints = [ - LineChartDataPoint(value: 10), - LineChartDataPoint(value: 60) - ] - let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) - XCTAssertFalse(chartData.isGreaterThanTwo()) - } - - func testMultiIsGreaterThanTwoTrue() { - let chartData = MultiLineChartData(dataSets: - MultiLineDataSet(dataSets: [ - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 10), - LineChartDataPoint(value: 40), - LineChartDataPoint(value: 30), - LineChartDataPoint(value: 60) - ], - legendTitle: "Bob"), - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 50), - LineChartDataPoint(value: 60), - LineChartDataPoint(value: 80), - LineChartDataPoint(value: 100) - ], - legendTitle: "Bob") - ])) - - XCTAssertTrue(chartData.isGreaterThanTwo()) - } - - func testMultiIsGreaterThanTwoFalse() { - let chartData = MultiLineChartData(dataSets: - MultiLineDataSet(dataSets: [ - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 10), - ], - legendTitle: "Bob"), - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 50) - ], - legendTitle: "Bob") - ])) - - XCTAssertFalse(chartData.isGreaterThanTwo()) - } - - static var allTests = [ - // Single Line Chart Data - ("testMaxValue", testMaxValue), - ("testMinValue", testMinValue), - ("testAverage", testAverage), - ("testRange", testRange), - ("testIsGreaterThanTwoTrue", testIsGreaterThanTwoTrue), - ("testIsGreaterThanTwoFalse", testIsGreaterThanTwoFalse), - // Multi Line Chart Data - ("testMultiLineMaxValue", testMultiLineMaxValue), - ("testMultiLineMinValue", testMultiLineMinValue), - ("testMultiLineAverage", testMultiLineAverage), - ("testMultiLineRange", testMultiLineRange), - - // Labels - ("testLineGetYLabels", testLineGetYLabels), - ("testBarGetYLabels", testBarGetYLabels), - - // Chart Data - ("testMultiIsGreaterThanTwoTrue", testIsGreaterThanTwoTrue), - ("testMultiIsGreaterThanTwoFalse", testIsGreaterThanTwoFalse), - ] -} 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 From f73d47b593a71d916196a2b0b6de95225034ae30 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 25 Feb 2021 08:41:50 +0000 Subject: [PATCH 092/152] Add tests. --- .../BarCharts/BarChartTests.swift | 6 +- .../BarCharts/GroupedBarChartTests.swift | 6 +- .../BarCharts/StackedBarChartTests.swift | 6 +- .../LineCharts/LineChartPathTests.swift | 37 +++++ .../LineCharts/LineChartTests.swift | 47 ++++-- .../LineCharts/MultiLineChartTest.swift | 146 ++++++++---------- 6 files changed, 142 insertions(+), 106 deletions(-) diff --git a/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift index 76686b9f..ccd34a5d 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift @@ -66,10 +66,10 @@ final class BarChartTests: XCTestCase { // Data ("testBarMaxValue", testBarMaxValue), ("testBarMinValue", testBarMinValue), - ("testBarAverage", testBarAverage), - ("testBarRange", testBarRange), + ("testBarAverage", testBarAverage), + ("testBarRange", testBarRange), // Greater - ("testBarIsGreaterThanTwoTrue", testBarIsGreaterThanTwoTrue), + ("testBarIsGreaterThanTwoTrue", testBarIsGreaterThanTwoTrue), ("testBarIsGreaterThanTwoFalse", testBarIsGreaterThanTwoFalse), // Labels ("testBarGetYLabels", testBarGetYLabels), diff --git a/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift index 9aad1c80..c1113e93 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift @@ -119,10 +119,10 @@ final class GroupedBarChartTests: XCTestCase { // Data ("testGroupedBarMaxValue", testGroupedBarMaxValue), ("testGroupedBarMinValue", testGroupedBarMinValue), - ("testGroupedBarAverage", testGroupedBarAverage), - ("testGroupedBarRange", testGroupedBarRange), + ("testGroupedBarAverage", testGroupedBarAverage), + ("testGroupedBarRange", testGroupedBarRange), // Greater - ("testGroupedBarIsGreaterThanTwoTrue", testGroupedBarIsGreaterThanTwoTrue), + ("testGroupedBarIsGreaterThanTwoTrue", testGroupedBarIsGreaterThanTwoTrue), ("testGroupedBarIsGreaterThanTwoFalse", testGroupedBarIsGreaterThanTwoFalse), // Labels ("testGroupedBarGetYLabels", testGroupedBarGetYLabels), diff --git a/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift index e44b43ab..11b86de5 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift @@ -119,10 +119,10 @@ final class StackedBarChartTests: XCTestCase { // Data ("testStackedBarMaxValue", testStackedBarMaxValue), ("testStackedBarMinValue", testStackedBarMinValue), - ("testStackedBarAverage", testStackedBarAverage), - ("testStackedBarRange", testStackedBarRange), + ("testStackedBarAverage", testStackedBarAverage), + ("testStackedBarRange", testStackedBarRange), // Greater - ("testStackedBarIsGreaterThanTwoTrue", testStackedBarIsGreaterThanTwoTrue), + ("testStackedBarIsGreaterThanTwoTrue", testStackedBarIsGreaterThanTwoTrue), ("testStackedBarIsGreaterThanTwoFalse", testStackedBarIsGreaterThanTwoFalse), // Labels ("testStackedBarGetYLabels", testStackedBarGetYLabels), diff --git a/Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift b/Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift index c1060e97..9c2d78b1 100644 --- a/Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift +++ b/Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift @@ -77,6 +77,40 @@ final class LineChartPathTests: XCTestCase { 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 = chartData.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 = chartData.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 = chartData.locationOnPath(0.5, path) + + XCTAssertEqual(test.x, 50, accuracy: 0.1) + XCTAssertEqual(test.y, 50, accuracy: 0.1) + } static var allTests = [ ("testGetIndicatorLocation", testGetIndicatorLocation), @@ -84,5 +118,8 @@ final class LineChartPathTests: XCTestCase { ("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 index 3e3bf653..5d0e790f 100644 --- a/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift +++ b/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift @@ -5,15 +5,15 @@ final class LineChartTests: XCTestCase { let dataPoints = [ LineChartDataPoint(value: 10), + LineChartDataPoint(value: 50), LineChartDataPoint(value: 40), - LineChartDataPoint(value: 30), - LineChartDataPoint(value: 60) + LineChartDataPoint(value: 80) ] // MARK: - Data func testLineMaxValue() { let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) - XCTAssertEqual(chartData.maxValue, 60) + XCTAssertEqual(chartData.maxValue, 80) } func testLineMinValue() { let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) @@ -21,11 +21,11 @@ final class LineChartTests: XCTestCase { } func testLineAverage() { let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) - XCTAssertEqual(chartData.average, 35) + XCTAssertEqual(chartData.average, 45) } func testLineRange() { let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) - XCTAssertEqual(chartData.range, 50.001) + XCTAssertEqual(chartData.range, 70.001) } // MARK: Greater @@ -45,13 +45,7 @@ final class LineChartTests: XCTestCase { // MARK: - Labels - func testLineGetYLabels() { - let dataPoints = [ - LineChartDataPoint(value: 10), - LineChartDataPoint(value: 50), - LineChartDataPoint(value: 40), - LineChartDataPoint(value: 80) - ] + func testLineGetYLabelsMinimumValue() { let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints), chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, baseline: .minimumValue)) @@ -63,6 +57,30 @@ final class LineChartTests: XCTestCase { } + func testLineGetYLabelsMinimumWithMax() { + let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints), + chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, + baseline: .minimumWithMaximum(of: 5))) + + XCTAssertEqual(chartData.getYLabels()[0], 5.0000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[1], 30.000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[2], 55.000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[3], 80.0000, accuracy: 0.01) + + } + + func testLineGetYLabelsZero() { + let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints), + chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, + baseline: .zero)) + + XCTAssertEqual(chartData.getYLabels()[0], 0.0000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[1], 26.666, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[2], 53.333, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[3], 80.0000, accuracy: 0.01) + + } + static var allTests = [ // Data @@ -74,6 +92,9 @@ final class LineChartTests: XCTestCase { ("testLineIsGreaterThanTwoTrue", testLineIsGreaterThanTwoTrue), ("testLineIsGreaterThanTwoFalse", testLineIsGreaterThanTwoFalse), // Labels - ("testLineGetYLabels", testLineGetYLabels), + ("testLineGetYLabelsMinimumValue", testLineGetYLabelsMinimumValue), + ("testLineGetYLabelsMinimumWithMax", testLineGetYLabelsMinimumWithMax), + ("testLineGetYLabelsZero", testLineGetYLabelsZero), ] } +//5+(80-5)/3*3 diff --git a/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift b/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift index 13c01667..c33716ae 100644 --- a/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift +++ b/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift @@ -3,102 +3,42 @@ import XCTest final class MultiLineChartTest: XCTestCase { + 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: - 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) - ]) - ])) + let chartData = MultiLineChartData(dataSets: dataSet) XCTAssertEqual(chartData.maxValue, 100) } func testMultiLineMinValue() { - let chartData = MultiLineChartData(dataSets: - 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) - ]) - ])) - + let chartData = MultiLineChartData(dataSets: dataSet) XCTAssertEqual(chartData.minValue, 10) } func testMultiLineAverage() { - let chartData = MultiLineChartData(dataSets: - 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) - ]) - ])) - + let chartData = MultiLineChartData(dataSets: dataSet) XCTAssertEqual(chartData.average, 53.75) } func testMultiLineRange() { - let chartData = MultiLineChartData(dataSets: - 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) - ]) - ])) - + let chartData = MultiLineChartData(dataSets: dataSet) XCTAssertEqual(chartData.range, 90.001) } // MARK: Greater func testMultiIsGreaterThanTwoTrue() { - let chartData = MultiLineChartData(dataSets: - 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) - ]) - ])) - + let chartData = MultiLineChartData(dataSets: dataSet) XCTAssertTrue(chartData.isGreaterThanTwo()) } @@ -112,20 +52,58 @@ final class MultiLineChartTest: XCTestCase { LineChartDataPoint(value: 50) ]) ])) - XCTAssertFalse(chartData.isGreaterThanTwo()) } + // MARK: - Labels + func testMultiLineGetYLabelsMinimumValue() { + let chartData = MultiLineChartData(dataSets: dataSet, + chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, + baseline: .minimumValue)) + + XCTAssertEqual(chartData.getYLabels()[0], 10.0000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[1], 40.0000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[2], 70.0000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[3], 100.0000, accuracy: 0.01) + + } + + func testMultiLineGetYLabelsMinimumWithMax() { + let chartData = MultiLineChartData(dataSets: dataSet, + chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, + baseline: .minimumWithMaximum(of: 5))) + + XCTAssertEqual(chartData.getYLabels()[0], 5.0000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[1], 36.6666, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[2], 68.3333, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[3], 100.0000, accuracy: 0.01) + + } + func testMultiLineGetYLabelsZero() { + let chartData = MultiLineChartData(dataSets: dataSet, + chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, + baseline: .zero)) + + XCTAssertEqual(chartData.getYLabels()[0], 0.0000, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[1], 33.3333, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[2], 66.6666, accuracy: 0.01) + XCTAssertEqual(chartData.getYLabels()[3], 100.0000, accuracy: 0.01) + + } static var allTests = [ // Data ("testMultiLineMaxValue", testMultiLineMaxValue), ("testMultiLineMinValue", testMultiLineMinValue), - ("testMultiLineAverage", testMultiLineAverage), - ("testMultiLineRange", testMultiLineRange), + ("testMultiLineAverage" , testMultiLineAverage), + ("testMultiLineRange" , testMultiLineRange), // Greater - ("testMultiLineIsGreaterThanTwoTrue", testMultiIsGreaterThanTwoTrue), + ("testMultiLineIsGreaterThanTwoTrue" , testMultiIsGreaterThanTwoTrue), ("testMultiLineIsGreaterThanTwoFalse", testMultiIsGreaterThanTwoFalse), + // Labels + ("testMultiLineGetYLabelsMinimumValue" , testMultiLineGetYLabelsMinimumValue), + ("testMultiLineGetYLabelsMinimumWithMax", testMultiLineGetYLabelsMinimumWithMax), + ("testMultiLineGetYLabelsZero" , testMultiLineGetYLabelsZero), ] } From 153e19c097e0679163d4db611e375fec32734b0b Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 25 Feb 2021 11:45:53 +0000 Subject: [PATCH 093/152] Change GeometryProxy to CGRect in all functions. --- .../Models/ChartData/BarChartData.swift | 16 ++++++------- .../ChartData/GroupedBarChartData.swift | 22 ++++++++--------- .../ChartData/StackedBarChartData.swift | 24 +++++++++---------- .../Models/ChartData/LineChartData.swift | 10 ++++---- .../Models/ChartData/MultiLineChartData.swift | 10 ++++---- .../Models/Protocols/LineChartProtocols.swift | 2 +- .../LineChartProtocolsExtensions.swift | 24 +++++++++---------- .../Models/ChartData/DoughnutChartData.swift | 10 ++++---- .../ChartData/MultiLayerPieChartData.swift | 8 +++---- .../Models/ChartData/PieChartData.swift | 10 ++++---- .../PieChartProtocolsExtentions.swift | 2 +- .../Models/Protocols/SharedProtocols.swift | 8 +++---- .../Shared/ViewModifiers/TouchOverlay.swift | 4 ++-- .../ViewModifiers/YAxisPOI.swift | 14 +++++------ 14 files changed, 82 insertions(+), 82 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index aa488dc9..6c7eec41 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -136,15 +136,15 @@ public final class BarChartData: CTBarChartDataProtocol { } // MARK: Touch - public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) { self.infoView.isTouchCurrent = true self.infoView.touchLocation = touchLocation - self.infoView.chartSize = chartSize.frame(in: .local) + self.infoView.chartSize = chartSize self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) } @ViewBuilder - public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { + public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { if let position = self.getPointLocation(dataSet: dataSets, touchLocation: touchLocation, @@ -185,9 +185,9 @@ public final class BarChartData: CTBarChartDataProtocol { // MARK: - Touch extension BarChartData: TouchProtocol { - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [BarChartDataPoint] = [] - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count) + let xSection : CGFloat = chartSize.width / CGFloat(dataSets.dataPoints.count) let index : Int = Int((touchLocation.x) / xSection) if index >= 0 && index < dataSets.dataPoints.count { points.append(dataSets.dataPoints[index]) @@ -195,9 +195,9 @@ extension BarChartData: TouchProtocol { self.infoView.touchOverlayInfo = points } - public func getPointLocation(dataSet: BarDataSet, touchLocation: CGPoint, chartSize: GeometryProxy) -> CGPoint? { - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count) - let ySection : CGFloat = chartSize.size.height / CGFloat(self.maxValue) + public func getPointLocation(dataSet: BarDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { + let xSection : CGFloat = chartSize.width / CGFloat(dataSets.dataPoints.count) + let ySection : CGFloat = chartSize.height / CGFloat(self.maxValue) let index : Int = Int((touchLocation.x) / xSection) if index >= 0 && index < dataSets.dataPoints.count { return CGPoint(x: (CGFloat(index) * xSection) + (xSection / 2), diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 30b7a330..ca7e6557 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -182,15 +182,15 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { } // MARK: Touch - public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) { self.infoView.isTouchCurrent = true self.infoView.touchLocation = touchLocation - self.infoView.chartSize = chartSize.frame(in: .local) + self.infoView.chartSize = chartSize self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) } @ViewBuilder - public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { + public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { if let position = self.getPointLocation(dataSet: dataSets, touchLocation: touchLocation, @@ -232,19 +232,19 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { // MARK: - Touch extension GroupedBarChartData: TouchProtocol { - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [MultiBarChartDataPoint] = [] // Divide the chart into equal sections. - let superXSection : CGFloat = (chartSize.size.width / CGFloat(dataSets.dataSets.count)) + 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.size.width / CGFloat(dataSets.dataSets.count)) - compensation + 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 { @@ -258,18 +258,18 @@ extension GroupedBarChartData: TouchProtocol { self.infoView.touchOverlayInfo = points } - public func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: GeometryProxy) -> CGPoint? { + public func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { // Divide the chart into equal sections. - let superXSection : CGFloat = (chartSize.size.width / CGFloat(dataSet.dataSets.count)) + 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.size.width / CGFloat(dataSet.dataSets.count)) - compensation - let ySection : CGFloat = chartSize.size.height / CGFloat(self.maxValue) + 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) @@ -284,7 +284,7 @@ extension GroupedBarChartData: TouchProtocol { let section : CGFloat = (superXSection * CGFloat(superIndex)) let spacing : CGFloat = ((groupSpacing / CGFloat(dataSets.dataSets.count)) * CGFloat(superIndex)) return CGPoint(x: element + section + spacing, - y: (chartSize.size.height - CGFloat(subDataSet.dataPoints[subIndex].value) * ySection)) + y: (chartSize.height - CGFloat(subDataSet.dataPoints[subIndex].value) * ySection)) } } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index a34dd659..50b8c1e8 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -168,15 +168,15 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { } // MARK: Touch - public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) { self.infoView.isTouchCurrent = true self.infoView.touchLocation = touchLocation - self.infoView.chartSize = chartSize.frame(in: .local) + self.infoView.chartSize = chartSize self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) } @ViewBuilder - public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { + public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { if let position = self.getPointLocation(dataSet: dataSets, touchLocation: touchLocation, @@ -217,12 +217,12 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { // MARK: - Touch extension StackedBarChartData: TouchProtocol { - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [MultiBarChartDataPoint] = [] // Filter to get the right dataset based on the x axis. - let superXSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataSets.count) + let superXSection : CGFloat = chartSize.width / CGFloat(dataSets.dataSets.count) let superIndex : Int = Int((touchLocation.x) / superXSection) if superIndex >= 0 && superIndex < dataSets.dataSets.count { @@ -239,7 +239,7 @@ extension StackedBarChartData: TouchProtocol { var heightOfElements : [CGFloat] = [] let sum = dataSet.dataPoints.reduce(0) { $0 + $1.value } dataSet.dataPoints.forEach { datapoint in - heightOfElements.append((chartSize.size.height * fraction) * CGFloat(datapoint.value / sum)) + heightOfElements.append((chartSize.height * fraction) * CGFloat(datapoint.value / sum)) } // Gets the highest point of each element. @@ -252,7 +252,7 @@ extension StackedBarChartData: TouchProtocol { endPointOfElements.append(returnValue) } - let yIndex = endPointOfElements.enumerated().first(where: { $0.element > abs(touchLocation.y - chartSize.size.height) }) + 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 { @@ -263,9 +263,9 @@ extension StackedBarChartData: TouchProtocol { self.infoView.touchOverlayInfo = points } - public func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: GeometryProxy) -> CGPoint? { + public func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { // Filter to get the right dataset based on the x axis. - let superXSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataSets.count) + let superXSection : CGFloat = chartSize.width / CGFloat(dataSet.dataSets.count) let superIndex : Int = Int((touchLocation.x) / superXSection) if superIndex >= 0 && superIndex < dataSet.dataSets.count { @@ -282,7 +282,7 @@ extension StackedBarChartData: TouchProtocol { var heightOfElements : [CGFloat] = [] let sum = subDataSet.dataPoints.reduce(0) { $0 + $1.value } subDataSet.dataPoints.forEach { datapoint in - heightOfElements.append((chartSize.size.height * fraction) * CGFloat(datapoint.value / sum)) + heightOfElements.append((chartSize.height * fraction) * CGFloat(datapoint.value / sum)) } // Gets the highest point of each element. @@ -296,14 +296,14 @@ extension StackedBarChartData: TouchProtocol { } let yIndex = endPointOfElements.enumerated().first(where: { - $0.element > abs(touchLocation.y - chartSize.size.height) + $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.size.height - endPointOfElements[index])) + y: (chartSize.height - endPointOfElements[index])) } } } diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index be15c15e..f852dbbc 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -154,16 +154,16 @@ public final class LineChartData: CTLineChartDataProtocol { } // MARK: Touch - public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) { self.infoView.isTouchCurrent = true self.infoView.touchLocation = touchLocation - self.infoView.chartSize = chartSize.frame(in: .local) + self.infoView.chartSize = chartSize self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) } - public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { + public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { self.markerSubView(dataSet: self.dataSets, touchLocation: touchLocation, chartSize: chartSize) } @@ -173,9 +173,9 @@ public final class LineChartData: CTLineChartDataProtocol { // MARK: - Touch extension LineChartData: TouchProtocol { - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [LineChartDataPoint] = [] - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSets.dataPoints.count - 1) + 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 { points.append(dataSets.dataPoints[index]) diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index e37be6d3..556fdf27 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -164,14 +164,14 @@ public final class MultiLineChartData: CTLineChartDataProtocol { } // MARK: Touch - public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) { self.infoView.isTouchCurrent = true self.infoView.touchLocation = touchLocation - self.infoView.chartSize = chartSize.frame(in: .local) + self.infoView.chartSize = chartSize self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) } - public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { + public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { ZStack { ForEach(self.dataSets.dataSets, id: \.self) { dataSet in self.markerSubView(dataSet: dataSet, touchLocation: touchLocation, chartSize: chartSize) @@ -187,10 +187,10 @@ public final class MultiLineChartData: CTLineChartDataProtocol { // MARK: - Touch extension MultiLineChartData: TouchProtocol { - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [LineChartDataPoint] = [] for dataSet in dataSets.dataSets { - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) + 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 { points.append(dataSet.dataPoints[index]) diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index c2342545..64cd310e 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -42,7 +42,7 @@ public protocol CTLineChartDataProtocol: CTLineBarChartDataProtocol { /// - touchLocation: Current location of the touch. /// - chartSize: The size of the chart view as the parent view. /// - Returns: Relevent touch marker based the chosen parameters. - func markerSubView(dataSet: LineDataSet, touchLocation: CGPoint, chartSize: GeometryProxy) -> Marker + func markerSubView(dataSet: LineDataSet, touchLocation: CGPoint, chartSize: CGRect) -> Marker /// Displays Shapes over the data points. /// - Returns: Relevent view containing point markers based the chosen parameters. diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index ddbfa25d..1b298226 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -238,17 +238,17 @@ extension CTLineChartDataProtocol { // MARK: - Markers extension CTLineChartDataProtocol { - public func getPointLocation(dataSet: LineDataSet, touchLocation: CGPoint, chartSize: GeometryProxy) -> CGPoint? { + public func getPointLocation(dataSet: LineDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { let minValue : Double = self.minValue let range : Double = self.range - let xSection : CGFloat = chartSize.size.width / CGFloat(dataSet.dataPoints.count - 1) - let ySection : CGFloat = chartSize.size.height / CGFloat(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 { return CGPoint(x: CGFloat(index) * xSection, - y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.size.height) + y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.height) } return nil } @@ -257,7 +257,7 @@ extension CTLineChartDataProtocol { extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { @ViewBuilder public func markerSubView(dataSet : LineDataSet, touchLocation : CGPoint, - chartSize : GeometryProxy + chartSize : CGRect ) -> some View { switch self.chartStyle.markerType { @@ -269,7 +269,7 @@ extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { lineColour: style.lineColour, lineWidth: style.lineWidth) .frame(width: style.size, height: style.size) - .position(self.getIndicatorLocation(rect: chartSize.frame(in: .global), + .position(self.getIndicatorLocation(rect: chartSize, dataPoints: dataSet.dataPoints, touchLocation: touchLocation, lineType: dataSet.style.lineType)) @@ -279,7 +279,7 @@ extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { switch attach { case .line(dot: let indicator): - let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + let position = self.getIndicatorLocation(rect: chartSize, dataPoints: dataSet.dataPoints, touchLocation: touchLocation, lineType: dataSet.style.lineType) @@ -303,7 +303,7 @@ extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { switch attach { case .line(dot: let indicator): - let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + let position = self.getIndicatorLocation(rect: chartSize, dataPoints: dataSet.dataPoints, touchLocation: touchLocation, lineType: dataSet.style.lineType) @@ -329,7 +329,7 @@ extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { switch attach { case .line(dot: let indicator): - let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + let position = self.getIndicatorLocation(rect: chartSize, dataPoints: dataSet.dataPoints, touchLocation: touchLocation, lineType: dataSet.style.lineType) @@ -355,7 +355,7 @@ extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { switch attach { case .line(dot: let indicator): - let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + let position = self.getIndicatorLocation(rect: chartSize, dataPoints: dataSet.dataPoints, touchLocation: touchLocation, lineType: dataSet.style.lineType) @@ -381,7 +381,7 @@ extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { switch attach { case .line(dot: let indicator): - let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + let position = self.getIndicatorLocation(rect: chartSize, dataPoints: dataSet.dataPoints, touchLocation: touchLocation, lineType: dataSet.style.lineType) @@ -407,7 +407,7 @@ extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { switch attach { case .line(dot: let indicator): - let position = self.getIndicatorLocation(rect: chartSize.frame(in: .global), + let position = self.getIndicatorLocation(rect: chartSize, dataPoints: dataSet.dataPoints, touchLocation: touchLocation, lineType: dataSet.style.lineType) diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift index 3750f1a9..4dea87c9 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift @@ -68,14 +68,14 @@ public final class DoughnutChartData: CTDoughnutChartDataProtocol { } // MARK: Touch - public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) { self.infoView.isTouchCurrent = true self.infoView.touchLocation = touchLocation - self.infoView.chartSize = chartSize.frame(in: .local) + self.infoView.chartSize = chartSize self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) } - public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } + public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { EmptyView() } public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint @@ -84,9 +84,9 @@ public final class DoughnutChartData: CTDoughnutChartDataProtocol { // MARK: - Touch extension DoughnutChartData: TouchProtocol { - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [PieChartDataPoint] = [] - let touchDegree = degree(from: touchLocation, in: chartSize.frame(in: .local)) + 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 { diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift index 854d4c81..bd31ab5b 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift @@ -121,14 +121,14 @@ public final class MultiLayerPieChartData: CTMultiPieChartDataProtocol { } // MARK: Touch - public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) { self.infoView.isTouchCurrent = true self.infoView.touchLocation = touchLocation - self.infoView.chartSize = chartSize.frame(in: .local) + self.infoView.chartSize = chartSize self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) } - public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } + public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { EmptyView() } public typealias Set = MultiPieDataSet public typealias DataPoint = MultiPieDataPoint @@ -137,7 +137,7 @@ public final class MultiLayerPieChartData: CTMultiPieChartDataProtocol { // MARK: - Touch extension MultiLayerPieChartData: TouchProtocol { - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { let points : [MultiPieDataPoint] = [] self.infoView.touchOverlayInfo = points } diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift index 865b6881..b64dc20f 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift @@ -69,14 +69,14 @@ public final class PieChartData: CTPieChartDataProtocol { } // MARK: Touch - public func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) { self.infoView.isTouchCurrent = true self.infoView.touchLocation = touchLocation - self.infoView.chartSize = chartSize.frame(in: .local) + self.infoView.chartSize = chartSize self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) } - public func getTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) -> some View { EmptyView() } + public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { EmptyView() } public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint @@ -85,9 +85,9 @@ public final class PieChartData: CTPieChartDataProtocol { // MARK: - Touch extension PieChartData: TouchProtocol { - public func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) { + public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [PieChartDataPoint] = [] - let touchDegree = degree(from: touchLocation, in: chartSize.frame(in: .local)) + 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 { diff --git a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift index 975f7b94..5a07ca43 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift @@ -70,7 +70,7 @@ extension CTPieDoughnutChartDataProtocol where Set == MultiPieDataSet, DataPoint } extension CTPieDoughnutChartDataProtocol { - public func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: GeometryProxy) -> CGPoint? { + public func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { return nil } } diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 80b5413a..52c07f64 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -79,7 +79,7 @@ public protocol CTChartData: ObservableObject, Identifiable { func isGreaterThanTwo() -> Bool // MARK: Touch - func setTouchInteraction(touchLocation: CGPoint, chartSize: GeometryProxy) + func setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) /** Takes touch location and return a view based on the chart type and configuration. @@ -88,7 +88,7 @@ public protocol CTChartData: ObservableObject, Identifiable { - 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: GeometryProxy) -> Touch + func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> Touch } @@ -107,7 +107,7 @@ internal protocol TouchProtocol { - chartSize: The size of the chart view as the parent view. - Returns: Array of data points. */ - func getDataPoint(touchLocation: CGPoint, chartSize: GeometryProxy) + func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) /** Gets the location of the data point in the view. @@ -116,7 +116,7 @@ internal protocol TouchProtocol { - 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: GeometryProxy) -> CGPoint? + func getPointLocation(dataSet: SetPoint, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 64ea7eac..3c78739f 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -33,7 +33,7 @@ internal struct TouchOverlay: ViewModifier where T: CTChartData { .onChanged { (value) in chartData.setTouchInteraction(touchLocation: value.location, - chartSize: geo) + chartSize: geo.frame(in: .local)) } .onEnded { _ in @@ -43,7 +43,7 @@ internal struct TouchOverlay: ViewModifier where T: CTChartData { ) if chartData.infoView.isTouchCurrent { chartData.getTouchInteraction(touchLocation: chartData.infoView.touchLocation, - chartSize: geo) + chartSize: geo.frame(in: .local)) } } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index 93a7e67b..bf91ef10 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -114,10 +114,10 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { ) .ifElse(self.chartData.chartStyle.yAxisLabelPosition == .leading, if: { $0.position(x: -18, - y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo)) + y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo.frame(in: .local))) }, else: { $0.position(x: geo.size.width + 18, - y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo)) + y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo.frame(in: .local))) }) @@ -133,20 +133,20 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { .stroke(lineColour, style: strokeStyle) ) .position(x: geo.size.width / 2, - y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo)) + y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo.frame(in: .local))) .opacity(startAnimation ? 1 : 0) } } } - func getYPoint(chartType: ChartType, chartSize: GeometryProxy) -> CGFloat { + func getYPoint(chartType: ChartType, chartSize: CGRect) -> CGFloat { switch chartData.chartType.chartType { case .line: - let y = chartSize.size.height / CGFloat(range) + let y = chartSize.height / CGFloat(range) return (CGFloat(markerValue - minValue) * -y) + chartSize.size.height case .bar: - let y = chartSize.size.height / CGFloat(maxValue) - return chartSize.size.height - CGFloat(markerValue) * y + let y = chartSize.height / CGFloat(maxValue) + return chartSize.height - CGFloat(markerValue) * y case .pie: return 0 } From da92d7ff2ca1d90cddd802f23085bb704dfd878b Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 25 Feb 2021 11:46:28 +0000 Subject: [PATCH 094/152] Add touch tests. --- .../BarCharts/BarChartTests.swift | 42 ++++++++-- .../BarCharts/GroupedBarChartTests.swift | 50 +++++++++++- .../BarCharts/StackedBarChartTests.swift | 76 ++++++++++++++++++- .../LineCharts/LineChartTests.swift | 45 ++++++++--- .../LineCharts/MultiLineChartTest.swift | 43 +++++++++++ 5 files changed, 237 insertions(+), 19 deletions(-) diff --git a/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift index ccd34a5d..e21e52eb 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift @@ -10,7 +10,7 @@ final class BarChartTests: XCTestCase { BarChartDataPoint(value: 60) ] - // MARK: Data + // MARK: - Data func testBarMaxValue() { let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints)) XCTAssertEqual(chartData.maxValue, 60) @@ -27,8 +27,6 @@ final class BarChartTests: XCTestCase { let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints)) XCTAssertEqual(chartData.range, 50.001) } - - // MARK: Greater func testBarIsGreaterThanTwoTrue() { let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints)) XCTAssertTrue(chartData.isGreaterThanTwo()) @@ -43,7 +41,7 @@ final class BarChartTests: XCTestCase { XCTAssertFalse(chartData.isGreaterThanTwo()) } - // MARK: Labels + // MARK: - Labels func testBarGetYLabels() { let dataPoints = [ BarChartDataPoint(value: 10), @@ -60,6 +58,39 @@ final class BarChartTests: XCTestCase { XCTAssertEqual(chartData.getYLabels()[3], 80.0000, 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]) + } static var allTests = [ @@ -73,7 +104,8 @@ final class BarChartTests: XCTestCase { ("testBarIsGreaterThanTwoFalse", testBarIsGreaterThanTwoFalse), // Labels ("testBarGetYLabels", testBarGetYLabels), - + // Touch + ("testBarGetDataPoint", testBarGetDataPoint), ] } diff --git a/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift index c1113e93..a3a83f5f 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift @@ -103,7 +103,7 @@ final class GroupedBarChartTests: XCTestCase { XCTAssertFalse(chartData.isGreaterThanTwo()) } - // MARK: Labels + // MARK: - Labels func testGroupedBarGetYLabels() { let chartData = GroupedBarChartData(dataSets: data, groups: groups, chartStyle: BarChartStyle(yAxisNumberOfLabels: 3)) @@ -114,7 +114,51 @@ final class GroupedBarChartTests: XCTestCase { XCTAssertEqual(chartData.getYLabels()[3], 90.0000, 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]) + } static var allTests = [ // Data ("testGroupedBarMaxValue", testGroupedBarMaxValue), @@ -126,6 +170,8 @@ final class GroupedBarChartTests: XCTestCase { ("testGroupedBarIsGreaterThanTwoFalse", testGroupedBarIsGreaterThanTwoFalse), // Labels ("testGroupedBarGetYLabels", testGroupedBarGetYLabels), + // Touch + ("testMultiLineGetDataPoint", testGroupedBarGetDataPoint), ] diff --git a/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift index 11b86de5..e0cbb890 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift @@ -51,7 +51,7 @@ final class StackedBarChartTests: XCTestCase { MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", pointLabel: "Four One" , group: Group.one.data), MultiBarChartDataPoint(value: 80, 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) + MultiBarChartDataPoint(value: 50, xAxisLabel: "4.4", pointLabel: "Four Four" , group: Group.four.data) ]) ]) @@ -114,6 +114,76 @@ final class StackedBarChartTests: XCTestCase { XCTAssertEqual(chartData.getYLabels()[3], 90.0000, 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]) + } + static var allTests = [ // Data @@ -126,7 +196,7 @@ final class StackedBarChartTests: XCTestCase { ("testStackedBarIsGreaterThanTwoFalse", testStackedBarIsGreaterThanTwoFalse), // Labels ("testStackedBarGetYLabels", testStackedBarGetYLabels), - - + // Touch + ("testStackedBarGetDataPoint", testStackedBarGetDataPoint), ] } diff --git a/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift b/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift index 5d0e790f..9b4e6d89 100644 --- a/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift +++ b/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift @@ -27,13 +27,10 @@ final class LineChartTests: XCTestCase { let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) XCTAssertEqual(chartData.range, 70.001) } - - // MARK: Greater func testLineIsGreaterThanTwoTrue() { let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints)) XCTAssertTrue(chartData.isGreaterThanTwo()) } - func testLineIsGreaterThanTwoFalse() { let dataPoints = [ LineChartDataPoint(value: 10), @@ -43,13 +40,11 @@ final class LineChartTests: XCTestCase { XCTAssertFalse(chartData.isGreaterThanTwo()) } - // MARK: - Labels func testLineGetYLabelsMinimumValue() { let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints), chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, baseline: .minimumValue)) - XCTAssertEqual(chartData.getYLabels()[0], 10.0000, accuracy: 0.01) XCTAssertEqual(chartData.getYLabels()[1], 33.3333, accuracy: 0.01) XCTAssertEqual(chartData.getYLabels()[2], 56.6666, accuracy: 0.01) @@ -61,7 +56,6 @@ final class LineChartTests: XCTestCase { let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints), chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, baseline: .minimumWithMaximum(of: 5))) - XCTAssertEqual(chartData.getYLabels()[0], 5.0000, accuracy: 0.01) XCTAssertEqual(chartData.getYLabels()[1], 30.000, accuracy: 0.01) XCTAssertEqual(chartData.getYLabels()[2], 55.000, accuracy: 0.01) @@ -73,7 +67,6 @@ final class LineChartTests: XCTestCase { let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints), chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, baseline: .zero)) - XCTAssertEqual(chartData.getYLabels()[0], 0.0000, accuracy: 0.01) XCTAssertEqual(chartData.getYLabels()[1], 26.666, accuracy: 0.01) XCTAssertEqual(chartData.getYLabels()[2], 53.333, accuracy: 0.01) @@ -81,6 +74,39 @@ final class LineChartTests: XCTestCase { } + // 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]) + } static var allTests = [ // Data @@ -88,13 +114,14 @@ final class LineChartTests: XCTestCase { ("testLineMinValue", testLineMinValue), ("testLineAverage", testLineAverage), ("testLineRange", testLineRange), - // Greater - ("testLineIsGreaterThanTwoTrue", testLineIsGreaterThanTwoTrue), + ("testLineIsGreaterThanTwoTrue", testLineIsGreaterThanTwoTrue), ("testLineIsGreaterThanTwoFalse", testLineIsGreaterThanTwoFalse), // Labels ("testLineGetYLabelsMinimumValue", testLineGetYLabelsMinimumValue), ("testLineGetYLabelsMinimumWithMax", testLineGetYLabelsMinimumWithMax), ("testLineGetYLabelsZero", testLineGetYLabelsZero), + // Touch + ("testLineGetDataPoint", testLineGetDataPoint), ] } //5+(80-5)/3*3 diff --git a/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift b/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift index c33716ae..ef4d3a61 100644 --- a/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift +++ b/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift @@ -91,6 +91,47 @@ final class MultiLineChartTest: XCTestCase { XCTAssertEqual(chartData.getYLabels()[3], 100.0000, 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]) + } static var allTests = [ // Data @@ -105,5 +146,7 @@ final class MultiLineChartTest: XCTestCase { ("testMultiLineGetYLabelsMinimumValue" , testMultiLineGetYLabelsMinimumValue), ("testMultiLineGetYLabelsMinimumWithMax", testMultiLineGetYLabelsMinimumWithMax), ("testMultiLineGetYLabelsZero" , testMultiLineGetYLabelsZero), + // Touch + ("testMultiLineGetDataPoint", testMultiLineGetDataPoint), ] } From 16b199a5c22c6dacbe0b30541371e47950c1bf53 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 25 Feb 2021 16:52:25 +0000 Subject: [PATCH 095/152] Mark touch protocol functions as internal. --- .../BarChart/Models/ChartData/BarChartData.swift | 10 +++++----- .../Models/ChartData/GroupedBarChartData.swift | 6 ++---- .../Models/ChartData/StackedBarChartData.swift | 4 ++-- .../Models/ChartData/MultiLineChartData.swift | 2 +- .../Protocols/LineChartProtocolsExtensions.swift | 8 +++++--- .../PieChart/Models/ChartData/DoughnutChartData.swift | 2 +- .../Models/ChartData/MultiLayerPieChartData.swift | 2 +- .../PieChart/Models/ChartData/PieChartData.swift | 2 +- .../Models/Protocols/PieChartProtocolsExtentions.swift | 2 +- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 6c7eec41..52436282 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -185,7 +185,7 @@ public final class BarChartData: CTBarChartDataProtocol { // MARK: - Touch extension BarChartData: TouchProtocol { - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + internal 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) @@ -195,13 +195,13 @@ extension BarChartData: TouchProtocol { self.infoView.touchOverlayInfo = points } - public func getPointLocation(dataSet: BarDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { - let xSection : CGFloat = chartSize.width / CGFloat(dataSets.dataPoints.count) + internal 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 < dataSets.dataPoints.count { + if index >= 0 && index < dataSet.dataPoints.count { return CGPoint(x: (CGFloat(index) * xSection) + (xSection / 2), - y: (chartSize.size.height - CGFloat(dataSets.dataPoints[index].value) * ySection)) + y: (chartSize.size.height - CGFloat(dataSet.dataPoints[index].value) * ySection)) } return nil } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index ca7e6557..ba289845 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -232,7 +232,7 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { // MARK: - Touch extension GroupedBarChartData: TouchProtocol { - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + internal func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [MultiBarChartDataPoint] = [] @@ -258,7 +258,7 @@ extension GroupedBarChartData: TouchProtocol { self.infoView.touchOverlayInfo = points } - public func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { + internal func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { // Divide the chart into equal sections. let superXSection : CGFloat = (chartSize.width / CGFloat(dataSet.dataSets.count)) @@ -285,10 +285,8 @@ extension GroupedBarChartData: TouchProtocol { 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 } } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index 50b8c1e8..309d3843 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -217,7 +217,7 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { // MARK: - Touch extension StackedBarChartData: TouchProtocol { - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + internal func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [MultiBarChartDataPoint] = [] @@ -263,7 +263,7 @@ extension StackedBarChartData: TouchProtocol { self.infoView.touchOverlayInfo = points } - public func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { + internal 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) diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index 556fdf27..ecc84cda 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -187,7 +187,7 @@ public final class MultiLineChartData: CTLineChartDataProtocol { // MARK: - Touch extension MultiLineChartData: TouchProtocol { - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + internal func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [LineChartDataPoint] = [] for dataSet in dataSets.dataSets { let xSection : CGFloat = chartSize.width / CGFloat(dataSet.dataPoints.count - 1) diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index 1b298226..5031c878 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -245,6 +245,7 @@ extension CTLineChartDataProtocol { 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 { return CGPoint(x: CGFloat(index) * xSection, @@ -255,9 +256,10 @@ extension CTLineChartDataProtocol { } extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { - @ViewBuilder public func markerSubView(dataSet : LineDataSet, - touchLocation : CGPoint, - chartSize : CGRect + @ViewBuilder + public func markerSubView(dataSet : LineDataSet, + touchLocation : CGPoint, + chartSize : CGRect ) -> some View { switch self.chartStyle.markerType { diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift index 4dea87c9..f46585ee 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift @@ -84,7 +84,7 @@ public final class DoughnutChartData: CTDoughnutChartDataProtocol { // MARK: - Touch extension DoughnutChartData: TouchProtocol { - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + internal func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [PieChartDataPoint] = [] let touchDegree = degree(from: touchLocation, in: chartSize) diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift index bd31ab5b..bb8a6b64 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift @@ -137,7 +137,7 @@ public final class MultiLayerPieChartData: CTMultiPieChartDataProtocol { // MARK: - Touch extension MultiLayerPieChartData: TouchProtocol { - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + internal func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { let points : [MultiPieDataPoint] = [] self.infoView.touchOverlayInfo = points } diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift index b64dc20f..d74a63e3 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift @@ -85,7 +85,7 @@ public final class PieChartData: CTPieChartDataProtocol { // MARK: - Touch extension PieChartData: TouchProtocol { - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + internal func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [PieChartDataPoint] = [] let touchDegree = degree(from: touchLocation, in: chartSize) diff --git a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift index 5a07ca43..58c7c764 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift @@ -70,7 +70,7 @@ extension CTPieDoughnutChartDataProtocol where Set == MultiPieDataSet, DataPoint } extension CTPieDoughnutChartDataProtocol { - public func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { + internal func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { return nil } } From 0cc74a1da9a0b58360d9c932041be40a6bfbff91 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 25 Feb 2021 16:52:47 +0000 Subject: [PATCH 096/152] Tidy up. --- .../SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 52c07f64..68605e12 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -112,6 +112,7 @@ internal protocol TouchProtocol { /** 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. From bda1bbdfed753a879ffa5d2083bc831035688cc8 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 25 Feb 2021 16:53:06 +0000 Subject: [PATCH 097/152] Update touch tests. --- .../BarCharts/BarChartTests.swift | 31 ++++++- .../BarCharts/GroupedBarChartTests.swift | 53 +++++++++++- .../BarCharts/StackedBarChartTests.swift | 83 ++++++++++++++++++- .../LineCharts/LineChartTests.swift | 31 ++++++- .../LineCharts/MultiLineChartTest.swift | 47 +++++++++++ 5 files changed, 235 insertions(+), 10 deletions(-) diff --git a/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift index e21e52eb..86e9507b 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift @@ -2,7 +2,7 @@ import XCTest @testable import SwiftUICharts final class BarChartTests: XCTestCase { - + // MARK: - Set Up let dataPoints = [ BarChartDataPoint(value: 10), BarChartDataPoint(value: 40), @@ -91,8 +91,32 @@ final class BarChartTests: XCTestCase { 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), @@ -105,7 +129,8 @@ final class BarChartTests: XCTestCase { // Labels ("testBarGetYLabels", testBarGetYLabels), // Touch - ("testBarGetDataPoint", testBarGetDataPoint), + ("testBarGetDataPoint", testBarGetDataPoint), + ("testBarGetPointLocation", testBarGetPointLocation), ] } diff --git a/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift index a3a83f5f..34ca12f0 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift @@ -3,6 +3,7 @@ import XCTest final class GroupedBarChartTests: XCTestCase { + // MARK: - Set Up enum Group { case one case two @@ -159,6 +160,54 @@ final class GroupedBarChartTests: XCTestCase { 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), @@ -171,8 +220,8 @@ final class GroupedBarChartTests: XCTestCase { // Labels ("testGroupedBarGetYLabels", testGroupedBarGetYLabels), // Touch - ("testMultiLineGetDataPoint", testGroupedBarGetDataPoint), - + ("testMultiLineGetDataPoint", testGroupedBarGetDataPoint), + ("testGroupedBarGetPointLocation", testGroupedBarGetPointLocation), ] } diff --git a/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift index e0cbb890..5f775575 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift @@ -3,6 +3,7 @@ import XCTest final class StackedBarChartTests: XCTestCase { + // MARK: - Set Up enum Group { case one case two @@ -184,7 +185,84 @@ final class StackedBarChartTests: XCTestCase { 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), @@ -197,6 +275,7 @@ final class StackedBarChartTests: XCTestCase { // Labels ("testStackedBarGetYLabels", testStackedBarGetYLabels), // Touch - ("testStackedBarGetDataPoint", testStackedBarGetDataPoint), + ("testStackedBarGetDataPoint", testStackedBarGetDataPoint), + ("testStackedBarGetPointLocation", testStackedBarGetPointLocation), ] } diff --git a/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift b/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift index 9b4e6d89..1318bce5 100644 --- a/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift +++ b/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift @@ -3,6 +3,7 @@ import XCTest final class LineChartTests: XCTestCase { + // MARK: - Set Up let dataPoints = [ LineChartDataPoint(value: 10), LineChartDataPoint(value: 50), @@ -107,7 +108,31 @@ final class LineChartTests: XCTestCase { 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), @@ -121,7 +146,7 @@ final class LineChartTests: XCTestCase { ("testLineGetYLabelsMinimumWithMax", testLineGetYLabelsMinimumWithMax), ("testLineGetYLabelsZero", testLineGetYLabelsZero), // Touch - ("testLineGetDataPoint", testLineGetDataPoint), + ("testLineGetDataPoint", testLineGetDataPoint), + ("testLineGetPointLocation", testLineGetPointLocation), ] } -//5+(80-5)/3*3 diff --git a/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift b/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift index ef4d3a61..d8003c25 100644 --- a/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift +++ b/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift @@ -3,6 +3,7 @@ import XCTest final class MultiLineChartTest: XCTestCase { + // MARK: - Set Up let dataSet = MultiLineDataSet(dataSets: [ LineDataSet(dataPoints: [ LineChartDataPoint(value: 10), @@ -133,6 +134,51 @@ final class MultiLineChartTest: XCTestCase { 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), @@ -148,5 +194,6 @@ final class MultiLineChartTest: XCTestCase { ("testMultiLineGetYLabelsZero" , testMultiLineGetYLabelsZero), // Touch ("testMultiLineGetDataPoint", testMultiLineGetDataPoint), + ("testMultiLineGetPointLocation", testMultiLineGetPointLocation), ] } From 7a84243dd2b57f1239fd3331bfa95d7559fcedcc Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 25 Feb 2021 18:24:33 +0000 Subject: [PATCH 098/152] Add accessibility labels. --- .../LineChart/Models/ChartData/LineChartData.swift | 1 + Sources/SwiftUICharts/LineChart/Views/LineChartView.swift | 4 +++- Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index f852dbbc..9a7ef020 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -165,6 +165,7 @@ public final class LineChartData: CTLineChartDataProtocol { public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { self.markerSubView(dataSet: self.dataSets, touchLocation: touchLocation, chartSize: chartSize) + .accessibility(label: Text("Touch Box")) } public typealias Set = LineDataSet diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index 83c318fb..61d7cbda 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -71,6 +71,7 @@ public struct LineChart: View where ChartData: LineChartData { range : range, colour : colour, isFilled : false) + .accessibility(label: Text("Line Chart")) } else if chartData.dataSets.style.colourType == .gradientColour, let colours = chartData.dataSets.style.colours, @@ -86,6 +87,7 @@ public struct LineChart: View where ChartData: LineChartData { startPoint : startPoint, endPoint : endPoint, isFilled : false) + .accessibility(label: Text("Line Chart")) } else if chartData.dataSets.style.colourType == .gradientStops, let stops = chartData.dataSets.style.stops, @@ -102,7 +104,7 @@ public struct LineChart: View where ChartData: LineChartData { startPoint: startPoint, endPoint : endPoint, isFilled : false) - + .accessibility(label: Text("Line Chart")) } } else { CustomNoDataView(chartData: chartData) } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 3c78739f..7901a025 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -28,6 +28,7 @@ internal struct TouchOverlay: ViewModifier where T: CTChartData { GeometryReader { geo in ZStack { content + .accessibility(label: Text("DragGesture")) .gesture( DragGesture(minimumDistance: 0) .onChanged { (value) in From 1982bce3d560007a77e8c2eb71f3710b1ed64675 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sat, 27 Feb 2021 09:51:19 +0000 Subject: [PATCH 099/152] Add accessibility modifiers. --- .../Models/ChartData/BarChartData.swift | 4 + .../ChartData/GroupedBarChartData.swift | 4 + .../ChartData/StackedBarChartData.swift | 5 + .../BarChart/Views/BarChart.swift | 3 +- .../BarChart/Views/GroupedBarChart.swift | 9 +- .../BarChart/Views/StackedBarChart.swift | 3 +- .../Views/SubViews/BarChartSubViews.swift | 14 +- .../BarChart/Views/SubViews/Bars.swift | 38 ++++- .../Models/ChartData/LineChartData.swift | 20 ++- .../Models/ChartData/MultiLineChartData.swift | 25 +++ .../LineChart/Models/LineChartDataPoint.swift | 8 +- .../Models/Protocols/LineChartProtocols.swift | 4 + .../LineChart/Views/FilledLineChart.swift | 85 +++++----- .../LineChart/Views/LineChartView.swift | 85 +++++----- .../LineChart/Views/MultiLineChart.swift | 3 + .../Views/SubViews/LineChartSubViews.swift | 155 +++++++++++------- .../Shared/Models/ChartMetadata.swift | 16 ++ .../Shared/Models/InfoViewData.swift | 10 ++ .../Shared/ViewModifiers/HeaderBox.swift | 3 + .../Shared/ViewModifiers/InfoBox.swift | 40 ++--- .../Shared/ViewModifiers/Legends.swift | 1 + .../Shared/ViewModifiers/TouchOverlay.swift | 1 - .../Shared/Views/LegendView.swift | 29 ++++ .../Shared/Views/TouchOverlayBox.swift | 15 +- .../ViewModifiers/XAxisLabels.swift | 8 +- .../ViewModifiers/YAxisLabels.swift | 2 + .../ViewModifiers/YAxisPOI.swift | 5 +- 27 files changed, 387 insertions(+), 208 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 52436282..8f35b90f 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -101,6 +101,8 @@ public final class BarChartData: CTBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) + .accessibility(label: Text("X Axis Label")) + .accessibility(value: Text("\(data.xAxisLabel ?? "")")) Spacer() .frame(minWidth: 0, maxWidth: 500) } @@ -118,6 +120,8 @@ public final class BarChartData: CTBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) + .accessibility(label: Text("X Axis Label")) + .accessibility(value: Text("\(data)")) Spacer() .frame(minWidth: 0, maxWidth: 500) } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index ba289845..294fd691 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -144,6 +144,8 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) + .accessibility(label: Text("XAxisLabel")) + .accessibility(value: Text("\(data.xAxisLabel ?? "")")) Spacer() .frame(minWidth: 0, maxWidth: 500) } @@ -164,6 +166,8 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) + .accessibility(label: Text("XAxisLabel")) + .accessibility(value: Text("\(data)")) Spacer() .frame(minWidth: 0, maxWidth: 500) } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index 309d3843..4f6cfb09 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -135,6 +135,9 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) + .accessibility(label: Text("X Axis Label")) + .accessibility(value: Text("\(group.title)")) + Spacer() .frame(minWidth: 0, maxWidth: 500) } @@ -150,6 +153,8 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) + .accessibility(label: Text("X Axis Label")) + .accessibility(value: Text("\(data)")) Spacer() .frame(minWidth: 0, maxWidth: 500) } diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift index 052bf904..9984f759 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift @@ -54,12 +54,13 @@ public struct BarChart: View where ChartData: BarChartData { BarChartDataSetSubView(chartData: chartData, dataPoint: dataPoint) + .accessibility(label: Text("\(chartData.metadata.title)")) case .dataPoints: BarChartDataPointSubView(chartData: chartData, dataPoint: dataPoint) - + .accessibility(label: Text("\(chartData.metadata.title)")) } } } diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index 14aad6fc..47fbace9 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -63,7 +63,8 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD let colour = dataPoint.group.colour { - ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) + .accessibility(label: Text("\(chartData.metadata.title)")) } else if dataPoint.group.colourType == .gradientColour, let colours = dataPoint.group.colours, @@ -71,7 +72,8 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD let endPoint = dataPoint.group.endPoint { - GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) + .accessibility(label: Text("\(chartData.metadata.title)")) } else if dataPoint.group.colourType == .gradientStops, let stops = dataPoint.group.stops, @@ -81,7 +83,8 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) + .accessibility(label: Text("\(chartData.metadata.title)")) } } diff --git a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift index b16a64df..5fddbce1 100644 --- a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift @@ -57,15 +57,16 @@ public struct StackedBarChart: View where ChartData: StackedBarChartD StackElementSubView(dataSet: dataSet) .scaleEffect(y: startAnimation ? CGFloat(DataFunctions.dataSetMaxValue(from: dataSet) / 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 } + .accessibility(label: 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 index 5256f3dc..a6b6f608 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift @@ -28,7 +28,7 @@ internal struct BarChartDataSetSubView: View { let colour = chartData.barStyle.colour { - ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) } else if chartData.barStyle.colourType == .gradientColour, let colours = chartData.barStyle.colours, @@ -36,7 +36,7 @@ internal struct BarChartDataSetSubView: View { let endPoint = chartData.barStyle.endPoint { - GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) } else if chartData.barStyle.colourType == .gradientStops, let stops = chartData.barStyle.stops, @@ -46,7 +46,7 @@ internal struct BarChartDataSetSubView: View { let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) } } @@ -74,7 +74,7 @@ internal struct BarChartDataPointSubView: View { let colour = dataPoint.colour { - ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) } else if dataPoint.colourType == .gradientColour, let colours = dataPoint.colours, @@ -82,7 +82,7 @@ internal struct BarChartDataPointSubView: View { let endPoint = dataPoint.endPoint { - GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) } else if dataPoint.colourType == .gradientStops, let stops = dataPoint.stops, @@ -92,9 +92,9 @@ internal struct BarChartDataPointSubView: View { let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) } else { - ColourBar(.blue, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth) + ColourBar(.blue, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) } } diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift index 4de8474e..a7c66599 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift @@ -16,26 +16,30 @@ import SwiftUI internal struct ColourBar: View { private let colour : Color - private let dataPoint : DP + private let data : DP private let maxValue : Double private let chartStyle : BarChartStyle private let cornerRadius: CornerRadius private let barWidth : CGFloat + private let specifier : String + internal init(_ colour : Color, _ dataPoint : DP, _ maxValue : Double, _ chartStyle : BarChartStyle, _ cornerRadius: CornerRadius, - _ barWidth : CGFloat + _ barWidth : CGFloat, + _ specifier : String ) { self.colour = colour - self.dataPoint = dataPoint + self.data = dataPoint self.maxValue = maxValue self.chartStyle = chartStyle self.cornerRadius = cornerRadius self.barWidth = barWidth + self.specifier = specifier } @State private var startAnimation : Bool = false @@ -46,14 +50,17 @@ internal struct ColourBar: View { bl: cornerRadius.bottom, br: cornerRadius.bottom) .fill(colour) - .scaleEffect(y: startAnimation ? CGFloat(dataPoint.value / maxValue) : 0, anchor: .bottom) + .scaleEffect(y: startAnimation ? CGFloat(data.value / maxValue) : 0, anchor: .bottom) .scaleEffect(x: barWidth, anchor: .center) + .background(Color(.gray).opacity(0.000000001)) .animateOnAppear(using: chartStyle.globalAnimation) { self.startAnimation = true } .animateOnDisappear(using: chartStyle.globalAnimation) { self.startAnimation = false } + + .accessibility(value: Text("\(data.value, specifier: specifier), \(data.pointDescription ?? "")")) } } @@ -74,6 +81,8 @@ internal struct GradientColoursBar: View { private let cornerRadius: CornerRadius private let barWidth : CGFloat + private let specifier : String + internal init(_ colours : [Color], _ startPoint : UnitPoint, _ endPoint : UnitPoint, @@ -81,7 +90,8 @@ internal struct GradientColoursBar: View { _ maxValue : Double, _ chartStyle : BarChartStyle, _ cornerRadius: CornerRadius, - _ barWidth : CGFloat + _ barWidth : CGFloat, + _ specifier : String ) { self.colours = colours self.startPoint = startPoint @@ -91,6 +101,7 @@ internal struct GradientColoursBar: View { self.chartStyle = chartStyle self.cornerRadius = cornerRadius self.barWidth = barWidth + self.specifier = specifier } @State private var startAnimation : Bool = false @@ -105,12 +116,14 @@ internal struct GradientColoursBar: View { endPoint: endPoint)) .scaleEffect(y: startAnimation ? CGFloat(data.value / maxValue) : 0, anchor: .bottom) .scaleEffect(x: barWidth, anchor: .center) + .background(Color(.gray).opacity(0.000000001)) .animateOnAppear(using: chartStyle.globalAnimation) { self.startAnimation = true } .animateOnDisappear(using: chartStyle.globalAnimation) { self.startAnimation = false } + .accessibility(value: Text("\(data.value, specifier: "%.f") \(data.pointDescription ?? "")")) } } @@ -131,6 +144,8 @@ internal struct GradientStopsBar: View { private let cornerRadius: CornerRadius private let barWidth : CGFloat + private let specifier : String + internal init(_ stops : [Gradient.Stop], _ startPoint : UnitPoint, _ endPoint : UnitPoint, @@ -138,7 +153,8 @@ internal struct GradientStopsBar: View { _ maxValue : Double, _ chartStyle : BarChartStyle, _ cornerRadius: CornerRadius, - _ barWidth : CGFloat + _ barWidth : CGFloat, + _ specifier : String ) { self.stops = stops self.startPoint = startPoint @@ -148,6 +164,7 @@ internal struct GradientStopsBar: View { self.chartStyle = chartStyle self.cornerRadius = cornerRadius self.barWidth = barWidth + self.specifier = specifier } @State private var startAnimation : Bool = false @@ -162,12 +179,14 @@ internal struct GradientStopsBar: View { endPoint: endPoint)) .scaleEffect(y: startAnimation ? CGFloat(data.value / maxValue) : 0, anchor: .bottom) .scaleEffect(x: barWidth, anchor: .center) + .background(Color(.gray).opacity(0.000000001)) .animateOnAppear(using: chartStyle.globalAnimation) { self.startAnimation = true } .animateOnDisappear(using: chartStyle.globalAnimation) { self.startAnimation = false } + .accessibility(value: Text("\(data.value, specifier: "%.f") \(data.pointDescription ?? "")")) } } @@ -288,7 +307,8 @@ internal struct StackElementSubView: View { ColourPartBar(colour, getHeight(height : geo.size.height, dataSet : dataSet, dataPoint : dataPoint)) - + .accessibility(value: Text("\(dataPoint.value, specifier: "%.f"), \(dataPoint.pointDescription ?? "")")) + } else if dataPoint.group.colourType == .gradientColour, let colours = dataPoint.group.colours, let startPoint = dataPoint.group.startPoint, @@ -298,7 +318,8 @@ internal struct StackElementSubView: View { GradientColoursPartBar(colours, startPoint, endPoint, getHeight(height: geo.size.height, dataSet : dataSet, dataPoint : dataPoint)) - + .accessibility(value: Text("\(dataPoint.value, specifier: "%.f") \(dataPoint.pointDescription ?? "")")) + } else if dataPoint.group.colourType == .gradientStops, let stops = dataPoint.group.stops, let startPoint = dataPoint.group.startPoint, @@ -310,6 +331,7 @@ internal struct StackElementSubView: View { GradientStopsPartBar(safeStops, startPoint, endPoint, getHeight(height: geo.size.height, dataSet : dataSet, dataPoint : dataPoint)) + .accessibility(value: Text("\(dataPoint.value, specifier: "%.f") \(dataPoint.pointDescription ?? "")")) } } diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index 9a7ef020..fc526a45 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -102,6 +102,8 @@ public final class LineChartData: CTLineChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) + .accessibility(label: Text("X Axis Label")) + .accessibility(value: Text("\(data.xAxisLabel ?? "")")) } if data != self.dataSets.dataPoints[self.dataSets.dataPoints.count - 1] { Spacer() @@ -120,6 +122,8 @@ public final class LineChartData: CTLineChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) + .accessibility(label: Text("X Axis Label")) + .accessibility(value: Text("\(data)")) if data != labelArray[labelArray.count - 1] { Spacer() .frame(minWidth: 0, maxWidth: 500) @@ -165,7 +169,21 @@ public final class LineChartData: CTLineChartDataProtocol { public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { self.markerSubView(dataSet: self.dataSets, touchLocation: touchLocation, chartSize: chartSize) - .accessibility(label: Text("Touch Box")) + } + + // MARK: Accessibility + 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)) + .accessibility(label: Text("\(self.metadata.title)")) + .accessibility(value: Text(String(format: self.infoView.touchSpecifier, + self.dataSets.dataPoints[point].value) + + ", \(self.dataSets.dataPoints[point].pointDescription ?? "")")) + } } public typealias Set = LineDataSet diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index ecc84cda..19af0b18 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -108,6 +108,8 @@ public final class MultiLineChartData: CTLineChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) + .accessibility(label: Text("X Axis Label")) + .accessibility(value: Text("\(data.xAxisLabel ?? "")")) } if data != self.dataSets.dataSets[0].dataPoints[self.dataSets.dataSets[0].dataPoints.count - 1] { Spacer() @@ -127,6 +129,8 @@ public final class MultiLineChartData: CTLineChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) + .accessibility(label: Text("X Axis Label")) + .accessibility(value: Text("\(data)")) if data != labelArray[labelArray.count - 1] { Spacer() .frame(minWidth: 0, maxWidth: 500) @@ -179,14 +183,35 @@ public final class MultiLineChartData: CTLineChartDataProtocol { } } + // 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)) + .accessibility(label: Text("\(self.metadata.title)")) + .accessibility(value: Text(String(format: self.infoView.touchSpecifier, + dataSet.dataPoints[point].value) + + ", \(dataSet.dataPoints[point].pointDescription ?? "")")) + } + } + } + public typealias Set = MultiLineDataSet public typealias DataPoint = LineChartDataPoint public typealias CTStyle = LineChartStyle } + // MARK: - Touch extension MultiLineChartData: TouchProtocol { + internal func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [LineChartDataPoint] = [] for dataSet in dataSets.dataSets { diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift b/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift index f735c362..5a345005 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift +++ b/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift @@ -27,6 +27,8 @@ public struct LineChartDataPoint: CTLineBarDataPoint { public var pointDescription : String? public var date : Date? + var testlabel : String + /// Data model for a single data point with colour for use with a line chart. /// - Parameters: /// - value: Value of the data point @@ -36,11 +38,15 @@ public struct LineChartDataPoint: CTLineBarDataPoint { public init(value : Double, xAxisLabel : String? = nil, pointLabel : String? = nil, - date : Date? = nil + date : Date? = nil, + + testlabel : String = "" ) { self.value = value self.xAxisLabel = xAxisLabel self.pointDescription = pointLabel self.date = date + + self.testlabel = testlabel } } diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index 64cd310e..835a31d1 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -18,6 +18,8 @@ public protocol CTLineChartDataProtocol: CTLineBarChartDataProtocol { /// A type representing opaque View associatedtype Points : View + associatedtype Access : View + /** Whether it is a normal or filled line. */ @@ -47,6 +49,8 @@ public protocol CTLineChartDataProtocol: CTLineBarChartDataProtocol { /// Displays Shapes over the data points. /// - Returns: Relevent view containing point markers based the chosen parameters. func getPointMarker() -> Points + + func getAccessibility() -> Access } diff --git a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift index 16c73b06..332cacd8 100644 --- a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift @@ -62,48 +62,53 @@ public struct FilledLineChart: View where ChartData: LineChartData { if chartData.isGreaterThanTwo() { - if chartData.dataSets.style.colourType == .colour, - let colour = chartData.dataSets.style.colour - { + ZStack { - LineChartColourSubView(chartData: chartData, - dataSet: chartData.dataSets, - minValue: minValue, - range: range, - colour: colour, - isFilled: true) - - } else if chartData.dataSets.style.colourType == .gradientColour, - let colours = chartData.dataSets.style.colours, - let startPoint = chartData.dataSets.style.startPoint, - let endPoint = chartData.dataSets.style.endPoint - { - - LineChartColoursSubView(chartData: chartData, - dataSet: chartData.dataSets, - minValue: minValue, - range: range, - colours: colours, - startPoint: startPoint, - endPoint: endPoint, - isFilled: true) - - } else if chartData.dataSets.style.colourType == .gradientStops, - let stops = chartData.dataSets.style.stops, - let startPoint = chartData.dataSets.style.startPoint, - let endPoint = chartData.dataSets.style.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) + chartData.getAccessibility() + if chartData.dataSets.style.colourType == .colour, + let colour = chartData.dataSets.style.colour + { + + LineChartColourSubView(chartData: chartData, + dataSet: chartData.dataSets, + minValue: minValue, + range: range, + colour: colour, + isFilled: true) + + } else if chartData.dataSets.style.colourType == .gradientColour, + let colours = chartData.dataSets.style.colours, + let startPoint = chartData.dataSets.style.startPoint, + let endPoint = chartData.dataSets.style.endPoint + { + + LineChartColoursSubView(chartData: chartData, + dataSet: chartData.dataSets, + minValue: minValue, + range: range, + colours: colours, + startPoint: startPoint, + endPoint: endPoint, + isFilled: true) + + } else if chartData.dataSets.style.colourType == .gradientStops, + let stops = chartData.dataSets.style.stops, + let startPoint = chartData.dataSets.style.startPoint, + let endPoint = chartData.dataSets.style.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/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index 61d7cbda..896008f2 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -62,49 +62,52 @@ public struct LineChart: View where ChartData: LineChartData { if chartData.isGreaterThanTwo() { - if chartData.dataSets.style.colourType == .colour, - let colour = chartData.dataSets.style.colour - { - LineChartColourSubView(chartData: chartData, - dataSet : chartData.dataSets, - minValue : minValue, - range : range, - colour : colour, - isFilled : false) - .accessibility(label: Text("Line Chart")) + ZStack { - } else if chartData.dataSets.style.colourType == .gradientColour, - let colours = chartData.dataSets.style.colours, - let startPoint = chartData.dataSets.style.startPoint, - let endPoint = chartData.dataSets.style.endPoint - { + chartData.getAccessibility() - LineChartColoursSubView(chartData : chartData, - dataSet : chartData.dataSets, - minValue : minValue, - range : range, - colours : colours, - startPoint : startPoint, - endPoint : endPoint, - isFilled : false) - .accessibility(label: Text("Line Chart")) - - } else if chartData.dataSets.style.colourType == .gradientStops, - let stops = chartData.dataSets.style.stops, - let startPoint = chartData.dataSets.style.startPoint, - let endPoint = chartData.dataSets.style.endPoint - { - let stops = GradientStop.convertToGradientStopsArray(stops: stops) - - LineChartStopsSubView(chartData : chartData, - dataSet : chartData.dataSets, - minValue : minValue, - range : range, - stops : stops, - startPoint: startPoint, - endPoint : endPoint, - isFilled : false) - .accessibility(label: Text("Line Chart")) + if chartData.dataSets.style.colourType == .colour, + let colour = chartData.dataSets.style.colour + { + LineChartColourSubView(chartData: chartData, + dataSet : chartData.dataSets, + minValue : minValue, + range : range, + colour : colour, + isFilled : false) + + + } else if chartData.dataSets.style.colourType == .gradientColour, + let colours = chartData.dataSets.style.colours, + let startPoint = chartData.dataSets.style.startPoint, + let endPoint = chartData.dataSets.style.endPoint + { + + LineChartColoursSubView(chartData : chartData, + dataSet : chartData.dataSets, + minValue : minValue, + range : range, + colours : colours, + startPoint : startPoint, + endPoint : endPoint, + isFilled : false) + + } else if chartData.dataSets.style.colourType == .gradientStops, + let stops = chartData.dataSets.style.stops, + let startPoint = chartData.dataSets.style.startPoint, + let endPoint = chartData.dataSets.style.endPoint + { + let stops = GradientStop.convertToGradientStopsArray(stops: stops) + + LineChartStopsSubView(chartData : chartData, + dataSet : chartData.dataSets, + minValue : minValue, + range : range, + stops : stops, + startPoint: startPoint, + endPoint : endPoint, + isFilled : false) + } } } else { CustomNoDataView(chartData: chartData) } } diff --git a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift index d57fdf00..97c29872 100644 --- a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift @@ -62,6 +62,9 @@ public struct MultiLineChart: View where ChartData: MultiLineChartDat if chartData.isGreaterThanTwo() { ZStack { + + chartData.getAccessibility() + ForEach(chartData.dataSets.dataSets, id: \.id) { dataSet in if dataSet.style.colourType == .colour, diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift index cd67cd53..a5785775 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift @@ -7,6 +7,28 @@ 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. @@ -48,12 +70,12 @@ internal struct LineChartColourSubView: View where CD: CTLineChartDataProtoc range : range) .ifElse(isFilled, if: { $0.scale(y: startAnimation ? 1 : 0, anchor: .bottom) - .fill(colour) + .fill(colour) }, else: { $0.trim(to: startAnimation ? 1 : 0) .stroke(colour, style: dataSet.style.strokeStyle.strokeToStrokeStyle()) }) - .background(Color(.gray).opacity(0.01)) + .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) { @@ -65,6 +87,7 @@ internal struct LineChartColourSubView: View where CD: CTLineChartDataProtoc } } + // MARK: - Gradient colour /** Sub view gets the line drawn, sets the colour and sets up the animations. @@ -75,13 +98,13 @@ internal struct LineChartColoursSubView: View where CD: CTLineChartDataProto private let chartData : CD private let dataSet : LineDataSet - + 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, @@ -107,36 +130,41 @@ internal struct LineChartColoursSubView: View where CD: CTLineChartDataProto internal var body: some View { - LineShape(dataPoints: dataSet.dataPoints, - lineType: dataSet.style.lineType, - isFilled: isFilled, - minValue: minValue, - range: range) - .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()) - }) + ZStack { + chartData.getAccessibility() - .background(Color(.gray).opacity(0.01)) - .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 - } + LineShape(dataPoints: dataSet.dataPoints, + lineType: dataSet.style.lineType, + isFilled: isFilled, + minValue: minValue, + range: range) + .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 + } + } } } @@ -182,36 +210,41 @@ internal struct LineChartStopsSubView: View where CD: CTLineChartDataProtoco internal var body: some View { - LineShape(dataPoints: dataSet.dataPoints, - lineType: dataSet.style.lineType, - isFilled: isFilled, - minValue: minValue, - range: range) + ZStack { - .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.01)) - .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 - } + chartData.getAccessibility() + + LineShape(dataPoints: dataSet.dataPoints, + lineType: dataSet.style.lineType, + isFilled: isFilled, + minValue: minValue, + range: range) + + .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/Shared/Models/ChartMetadata.swift b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift index 494833e0..4a57c526 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift @@ -44,3 +44,19 @@ public struct ChartMetadata { self.subtitleColour = subtitleColour } } +struct Accessibility { + #if os(iOS) + static func read(this value: String) { + if UIAccessibility.isVoiceOverRunning { + UIAccessibility.post(notification: .announcement, argument: "\(value)") + } + } + + #else + + static func read(this value: String) { + + } + + #endif +} diff --git a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift index f3e9df49..c48518a1 100644 --- a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift @@ -30,6 +30,16 @@ public struct InfoViewData { */ var touchOverlayInfo : [DP] = [] +// var accessibilityLabels : String { +// get { +// var _returnData = "" +// for info in touchOverlayInfo { +// _returnData += String(format: touchSpecifier, info.value) + "\(info.pointDescription ?? "")," +// } +// return _returnData +// } +// } + /** Set specifier of data point readout. diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index 515b4337..774e28e4 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -82,6 +82,9 @@ internal struct HeaderBox: ViewModifier where T: CTChartData { .frame(minWidth: 0, maxWidth: .infinity) } content +// .onChange(of: chartData.infoView.accessibilityLabels) { (value) in +// Accessibility.read(this: value) +// } } } } else { content } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift index fa6217a9..d240b7f7 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift @@ -27,6 +27,9 @@ internal struct InfoBox: ViewModifier where T: CTChartData { EmptyView() } content +// .onChange(of: chartData.infoView.accessibilityLabels) { (value) in +// Accessibility.read(this: value) +// } } } @@ -46,35 +49,16 @@ internal struct InfoBox: ViewModifier where T: CTChartData { var fixed: some View { - LazyHGrid(rows: [GridItem(.flexible())]) { - ForEach(chartData.infoView.touchOverlayInfo, id: \.self) { point in - HStack { - Text("\(point.value, specifier: chartData.infoView.touchSpecifier)") - .font(.subheadline) - .foregroundColor(chartData.chartStyle.infoBoxValueColour) - if let label = point.pointDescription { - Text(label) - .font(.subheadline) - .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) - } - } - } - }.frame(height: 40) + + TouchOverlayBox(isTouchCurrent : chartData.infoView.isTouchCurrent, + selectedPoints : chartData.infoView.touchOverlayInfo, + specifier : chartData.infoView.touchSpecifier, + valueColour : chartData.chartStyle.infoBoxValueColour, + descriptionColour: chartData.chartStyle.infoBoxDescriptionColour, + boxFrame : $boxFrame) + .frame(height: 40) .padding(.horizontal, 6) - .background( - Group { - if chartData.infoView.isTouchCurrent { - RoundedRectangle(cornerRadius: 5.0, style: .continuous) - .fill(Color.systemsBackground) - .overlay( - Group { - RoundedRectangle(cornerRadius: 5.0) - .stroke(Color.primary, lineWidth: 1) - } - ) - } - } - ) + } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift index 976f584f..026195f7 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/Legends.swift @@ -31,6 +31,7 @@ internal struct Legends: ViewModifier where T: CTChartData { VStack { content LegendView(chartData: chartData, columns: columns, textColor: textColor) + } } else { content } } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 7901a025..3c78739f 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -28,7 +28,6 @@ internal struct TouchOverlay: ViewModifier where T: CTChartData { GeometryReader { geo in ZStack { content - .accessibility(label: Text("DragGesture")) .gesture( DragGesture(minimumDistance: 0) .onChanged { (value) in diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index 6c6b3e08..6cf1f01b 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -35,22 +35,51 @@ internal struct LegendView: View where T: CTChartData { case .line: line(legend) + .accessibility(label: Text(accessibilityLegendLabel(legend: legend))) + .accessibility(value: Text("\(legend.legend)")) case .bar: bar(legend) .if(scaleLegendBar(legend: legend)) { $0.scaleEffect(1.2, anchor: .leading) } + .accessibility(label: Text(accessibilityLegendLabel(legend: legend))) + .accessibility(value: Text("\(legend.legend)")) case .pie: pie(legend) .if(scaleLegendPie(legend: legend)) { $0.scaleEffect(1.2, anchor: .leading) } + .accessibility(label: Text(accessibilityLegendLabel(legend: legend))) + .accessibility(value: Text("\(legend.legend)")) } } }.id(UUID()) } + private func accessibilityLegendLabel(legend: LegendData) -> String { + switch legend.chartType { + case .line: + if legend.prioity == 1 { + return "Line Chart Legend" + } else { + return "P O I Marker Legend" + } + case .bar: + if legend.prioity == 1 { + return "Bar Chart Legend" + } else { + return "P O I Marker Legend" + } + case .pie: + if legend.prioity == 1 { + return "Pie Chart Legend" + } else { + return "P O I Marker Legend" + } + } + } + /// Detects whether to run the scale effect on the legend. private func scaleLegendBar(legend: LegendData) -> Bool { diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index 43f81542..ed9a5327 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -39,20 +39,14 @@ internal struct TouchOverlayBox: View { self._boxFrame = boxFrame self.ignoreZero = ignoreZero } - + internal var body: some View { HStack { ForEach(selectedPoints, id: \.self) { point in - if ignoreZero && point.value != 0 { - Text("\(point.value, specifier: specifier)") - .font(.subheadline) - .foregroundColor(valueColour) - } else if !ignoreZero { - Text("\(point.value, specifier: specifier)") - .font(.subheadline) - .foregroundColor(valueColour) - } + Text("\(point.value, specifier: specifier)") + .font(.subheadline) + .foregroundColor(valueColour) if let label = point.pointDescription { Text(label) .font(.subheadline) @@ -60,6 +54,7 @@ internal struct TouchOverlayBox: View { } } } + .padding(.all, 8) .background( GeometryReader { geo in diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift index 618b49fc..2db9425f 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift @@ -23,19 +23,19 @@ internal struct XAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol Group { switch chartData.chartStyle.xAxisLabelPosition { case .top: -// if chartData.isGreaterThanTwo() { + if chartData.isGreaterThanTwo() { VStack { chartData.getXAxisLabels() content } -// } else { content } + } else { content } case .bottom: -// if chartData.isGreaterThanTwo() { + if chartData.isGreaterThanTwo() { VStack { content chartData.getXAxisLabels() } -// } else { content } + } else { content } } } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift index ae1b835d..24e9848d 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift @@ -49,6 +49,8 @@ internal struct YAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol .foregroundColor(chartData.chartStyle.yAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) + .accessibility(label: Text("Y Axis Label")) + .accessibility(value: Text("\(labelsArray[i], specifier: specifier)")) if i != 0 { Spacer() .frame(minHeight: 0, maxHeight: 500) diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index bf91ef10..74292c42 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -119,7 +119,8 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { $0.position(x: geo.size.width + 18, y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo.frame(in: .local))) }) - + .accessibility(label: Text("P O I Marker")) + .accessibility(value: Text("\(markerName), \(markerValue, specifier: specifier)")) case .center(specifier: let specifier): @@ -135,6 +136,8 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { .position(x: geo.size.width / 2, y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo.frame(in: .local))) .opacity(startAnimation ? 1 : 0) + .accessibility(label: Text("P O I Marker")) + .accessibility(value: Text("\(markerName), \(markerValue, specifier: specifier)")) } } } From 1e53a33c80b81065fa35cdea1bb7063e5d47881b Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sat, 27 Feb 2021 14:07:13 +0000 Subject: [PATCH 100/152] Improve accessibility. --- .../BarChart/Models/ChartData/BarChartData.swift | 8 ++++---- .../Models/ChartData/GroupedBarChartData.swift | 8 ++++---- .../Models/ChartData/StackedBarChartData.swift | 8 ++++---- Sources/SwiftUICharts/BarChart/Views/BarChart.swift | 4 ++-- .../BarChart/Views/GroupedBarChart.swift | 6 +++--- .../BarChart/Views/StackedBarChart.swift | 2 +- .../SwiftUICharts/BarChart/Views/SubViews/Bars.swift | 12 ++++++------ .../LineChart/Models/ChartData/LineChartData.swift | 12 ++++++------ .../Models/ChartData/MultiLineChartData.swift | 12 ++++++------ .../SwiftUICharts/PieChart/Views/DoughnutChart.swift | 4 +++- .../PieChart/Views/MultiLayerPieChart.swift | 11 ++++++++--- Sources/SwiftUICharts/PieChart/Views/PieChart.swift | 2 ++ Sources/SwiftUICharts/Shared/Views/LegendView.swift | 12 ++++++------ .../SharedLineAndBar/ViewModifiers/YAxisLabels.swift | 4 ++-- .../SharedLineAndBar/ViewModifiers/YAxisPOI.swift | 8 ++++---- 15 files changed, 61 insertions(+), 52 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 8f35b90f..29a4a229 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -101,8 +101,8 @@ public final class BarChartData: CTBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibility(label: Text("X Axis Label")) - .accessibility(value: Text("\(data.xAxisLabel ?? "")")) + .accessibilityLabel( Text("X Axis Label")) + .accessibilityValue(Text("\(data.xAxisLabel ?? "")")) Spacer() .frame(minWidth: 0, maxWidth: 500) } @@ -120,8 +120,8 @@ public final class BarChartData: CTBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibility(label: Text("X Axis Label")) - .accessibility(value: Text("\(data)")) + .accessibilityLabel( Text("X Axis Label")) + .accessibilityValue(Text("\(data)")) Spacer() .frame(minWidth: 0, maxWidth: 500) } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 294fd691..109eb0d4 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -144,8 +144,8 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibility(label: Text("XAxisLabel")) - .accessibility(value: Text("\(data.xAxisLabel ?? "")")) + .accessibilityLabel( Text("XAxisLabel")) + .accessibilityValue(Text("\(data.xAxisLabel ?? "")")) Spacer() .frame(minWidth: 0, maxWidth: 500) } @@ -166,8 +166,8 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibility(label: Text("XAxisLabel")) - .accessibility(value: Text("\(data)")) + .accessibilityLabel( Text("XAxisLabel")) + .accessibilityValue(Text("\(data)")) Spacer() .frame(minWidth: 0, maxWidth: 500) } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index 4f6cfb09..b536f490 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -135,8 +135,8 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibility(label: Text("X Axis Label")) - .accessibility(value: Text("\(group.title)")) + .accessibilityLabel( Text("X Axis Label")) + .accessibilityValue(Text("\(group.title)")) Spacer() .frame(minWidth: 0, maxWidth: 500) @@ -153,8 +153,8 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibility(label: Text("X Axis Label")) - .accessibility(value: Text("\(data)")) + .accessibilityLabel( Text("X Axis Label")) + .accessibilityValue(Text("\(data)")) Spacer() .frame(minWidth: 0, maxWidth: 500) } diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift index 9984f759..3c9e1bd4 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift @@ -54,13 +54,13 @@ public struct BarChart: View where ChartData: BarChartData { BarChartDataSetSubView(chartData: chartData, dataPoint: dataPoint) - .accessibility(label: Text("\(chartData.metadata.title)")) + .accessibilityLabel( Text("\(chartData.metadata.title)")) case .dataPoints: BarChartDataPointSubView(chartData: chartData, dataPoint: dataPoint) - .accessibility(label: Text("\(chartData.metadata.title)")) + .accessibilityLabel( Text("\(chartData.metadata.title)")) } } } diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index 47fbace9..e935c198 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -64,7 +64,7 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD { ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) - .accessibility(label: Text("\(chartData.metadata.title)")) + .accessibilityLabel( Text("\(chartData.metadata.title)")) } else if dataPoint.group.colourType == .gradientColour, let colours = dataPoint.group.colours, @@ -73,7 +73,7 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD { GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) - .accessibility(label: Text("\(chartData.metadata.title)")) + .accessibilityLabel( Text("\(chartData.metadata.title)")) } else if dataPoint.group.colourType == .gradientStops, let stops = dataPoint.group.stops, @@ -84,7 +84,7 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) - .accessibility(label: Text("\(chartData.metadata.title)")) + .accessibilityLabel( Text("\(chartData.metadata.title)")) } } diff --git a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift index 5fddbce1..66294908 100644 --- a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift @@ -64,7 +64,7 @@ public struct StackedBarChart: View where ChartData: StackedBarChartD .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = false } - .accessibility(label: Text("\(chartData.metadata.title)")) + .accessibilityLabel( Text("\(chartData.metadata.title)")) } } } else { CustomNoDataView(chartData: chartData) } diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift index a7c66599..693a4d8f 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift @@ -60,7 +60,7 @@ internal struct ColourBar: View { self.startAnimation = false } - .accessibility(value: Text("\(data.value, specifier: specifier), \(data.pointDescription ?? "")")) + .accessibilityValue(Text("\(data.value, specifier: specifier), \(data.pointDescription ?? "")")) } } @@ -123,7 +123,7 @@ internal struct GradientColoursBar: View { .animateOnDisappear(using: chartStyle.globalAnimation) { self.startAnimation = false } - .accessibility(value: Text("\(data.value, specifier: "%.f") \(data.pointDescription ?? "")")) + .accessibilityValue(Text("\(data.value, specifier: "%.f") \(data.pointDescription ?? "")")) } } @@ -186,7 +186,7 @@ internal struct GradientStopsBar: View { .animateOnDisappear(using: chartStyle.globalAnimation) { self.startAnimation = false } - .accessibility(value: Text("\(data.value, specifier: "%.f") \(data.pointDescription ?? "")")) + .accessibilityValue(Text("\(data.value, specifier: "%.f") \(data.pointDescription ?? "")")) } } @@ -307,7 +307,7 @@ internal struct StackElementSubView: View { ColourPartBar(colour, getHeight(height : geo.size.height, dataSet : dataSet, dataPoint : dataPoint)) - .accessibility(value: Text("\(dataPoint.value, specifier: "%.f"), \(dataPoint.pointDescription ?? "")")) + .accessibilityValue(Text("\(dataPoint.value, specifier: "%.f"), \(dataPoint.pointDescription ?? "")")) } else if dataPoint.group.colourType == .gradientColour, let colours = dataPoint.group.colours, @@ -318,7 +318,7 @@ internal struct StackElementSubView: View { GradientColoursPartBar(colours, startPoint, endPoint, getHeight(height: geo.size.height, dataSet : dataSet, dataPoint : dataPoint)) - .accessibility(value: Text("\(dataPoint.value, specifier: "%.f") \(dataPoint.pointDescription ?? "")")) + .accessibilityValue(Text("\(dataPoint.value, specifier: "%.f") \(dataPoint.pointDescription ?? "")")) } else if dataPoint.group.colourType == .gradientStops, let stops = dataPoint.group.stops, @@ -331,7 +331,7 @@ internal struct StackElementSubView: View { GradientStopsPartBar(safeStops, startPoint, endPoint, getHeight(height: geo.size.height, dataSet : dataSet, dataPoint : dataPoint)) - .accessibility(value: Text("\(dataPoint.value, specifier: "%.f") \(dataPoint.pointDescription ?? "")")) + .accessibilityValue(Text("\(dataPoint.value, specifier: "%.f") \(dataPoint.pointDescription ?? "")")) } } diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index fc526a45..6d7c5419 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -102,8 +102,8 @@ public final class LineChartData: CTLineChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibility(label: Text("X Axis Label")) - .accessibility(value: Text("\(data.xAxisLabel ?? "")")) + .accessibilityLabel( Text("X Axis Label")) + .accessibilityValue(Text("\(data.xAxisLabel ?? "")")) } if data != self.dataSets.dataPoints[self.dataSets.dataPoints.count - 1] { Spacer() @@ -122,8 +122,8 @@ public final class LineChartData: CTLineChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibility(label: Text("X Axis Label")) - .accessibility(value: Text("\(data)")) + .accessibilityLabel( Text("X Axis Label")) + .accessibilityValue(Text("\(data)")) if data != labelArray[labelArray.count - 1] { Spacer() .frame(minWidth: 0, maxWidth: 500) @@ -179,8 +179,8 @@ public final class LineChartData: CTLineChartDataProtocol { dataPointNo : point) .foregroundColor(Color(.gray).opacity(0.000000001)) - .accessibility(label: Text("\(self.metadata.title)")) - .accessibility(value: Text(String(format: self.infoView.touchSpecifier, + .accessibilityLabel( Text("\(self.metadata.title)")) + .accessibilityValue(Text(String(format: self.infoView.touchSpecifier, self.dataSets.dataPoints[point].value) + ", \(self.dataSets.dataPoints[point].pointDescription ?? "")")) } diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index 19af0b18..bf166335 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -108,8 +108,8 @@ public final class MultiLineChartData: CTLineChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibility(label: Text("X Axis Label")) - .accessibility(value: Text("\(data.xAxisLabel ?? "")")) + .accessibilityLabel( Text("X Axis Label")) + .accessibilityValue(Text("\(data.xAxisLabel ?? "")")) } if data != self.dataSets.dataSets[0].dataPoints[self.dataSets.dataSets[0].dataPoints.count - 1] { Spacer() @@ -129,8 +129,8 @@ public final class MultiLineChartData: CTLineChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibility(label: Text("X Axis Label")) - .accessibility(value: Text("\(data)")) + .accessibilityLabel( Text("X Axis Label")) + .accessibilityValue(Text("\(data)")) if data != labelArray[labelArray.count - 1] { Spacer() .frame(minWidth: 0, maxWidth: 500) @@ -194,8 +194,8 @@ public final class MultiLineChartData: CTLineChartDataProtocol { dataPointNo : point) .foregroundColor(Color(.gray).opacity(0.000000001)) - .accessibility(label: Text("\(self.metadata.title)")) - .accessibility(value: Text(String(format: self.infoView.touchSpecifier, + .accessibilityLabel( Text("\(self.metadata.title)")) + .accessibilityValue(Text(String(format: self.infoView.touchSpecifier, dataSet.dataPoints[point].value) + ", \(dataSet.dataPoints[point].pointDescription ?? "")")) } diff --git a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift index 9d5510bb..28bc2d52 100644 --- a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift @@ -46,7 +46,7 @@ public struct DoughnutChart: View where ChartData: DoughnutChartData DoughnutSegmentShape(id: chartData.dataSets.dataPoints[data].id, startAngle: chartData.dataSets.dataPoints[data].startAngle, amount: chartData.dataSets.dataPoints[data].amount) - .stroke/*Border*/(chartData.dataSets.dataPoints[data].colour, lineWidth: chartData.chartStyle.strokeWidth) + .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)) @@ -56,6 +56,8 @@ public struct DoughnutChart: View where ChartData: DoughnutChartData .zIndex(1) .shadow(color: Color.primary, radius: 10) } + .accessibilityLabel(Text("\(chartData.metadata.title)")) + .accessibilityValue(Text(String(format: chartData.infoView.touchSpecifier, chartData.dataSets.dataPoints[data].value) + "\(chartData.dataSets.dataPoints[data].pointDescription ?? "")")) } } .animateOnAppear(using: chartData.chartStyle.globalAnimation) { diff --git a/Sources/SwiftUICharts/PieChart/Views/MultiLayerPieChart.swift b/Sources/SwiftUICharts/PieChart/Views/MultiLayerPieChart.swift index 7bdcfaab..894a2743 100644 --- a/Sources/SwiftUICharts/PieChart/Views/MultiLayerPieChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/MultiLayerPieChart.swift @@ -48,6 +48,8 @@ public struct MultiLayerPieChart: View where ChartData: MultiLayerPie startAngle: data.startAngle, amount: data.amount) .fill(data.colour) + .accessibilityLabel(Text("\(chartData.metadata.title)")) + .accessibilityValue(Text(String(format: chartData.infoView.touchSpecifier, data.value) + "\(data.pointDescription ?? "")")) if let points = data.layerDataPoints { ForEach(points, id: \.self) { point in @@ -55,8 +57,8 @@ public struct MultiLayerPieChart: View where ChartData: MultiLayerPie startAngle: point.startAngle, amount: point.amount) .strokeBorder(point.colour, lineWidth: 120) - - + .accessibilityLabel(Text("\(chartData.metadata.title)")) + .accessibilityValue(Text(String(format: chartData.infoView.touchSpecifier, point.value) + "\(point.pointDescription ?? "")")) if let pointsTwo = point.layerDataPoints { ForEach(pointsTwo, id: \.self) { pointTwo in @@ -64,7 +66,8 @@ public struct MultiLayerPieChart: View where ChartData: MultiLayerPie startAngle: pointTwo.startAngle, amount: pointTwo.amount) .strokeBorder(pointTwo.colour, lineWidth: 80) - + .accessibilityLabel(Text("\(chartData.metadata.title)")) + .accessibilityValue(Text(String(format: chartData.infoView.touchSpecifier, pointTwo.value) + "\(pointTwo.pointDescription ?? "")")) if let pointsThree = pointTwo.layerDataPoints { ForEach(pointsThree, id: \.self) { pointThree in @@ -72,6 +75,8 @@ public struct MultiLayerPieChart: View where ChartData: MultiLayerPie startAngle: pointThree.startAngle, amount: pointThree.amount) .strokeBorder(pointThree.colour, lineWidth: 40) + .accessibilityLabel(Text("\(chartData.metadata.title)")) + .accessibilityValue(Text(String(format: chartData.infoView.touchSpecifier, pointThree.value) + "\(pointThree.pointDescription ?? "")")) } } } diff --git a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift index e09b65fb..bfddc934 100644 --- a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift @@ -56,6 +56,8 @@ public struct PieChart: View where ChartData: PieChartData { .zIndex(1) .shadow(color: Color.primary, radius: 10) } + .accessibilityLabel(Text("\(chartData.metadata.title)")) + .accessibilityValue(Text(String(format: chartData.infoView.touchSpecifier, chartData.dataSets.dataPoints[data].value) + "\(chartData.dataSets.dataPoints[data].pointDescription ?? "")")) } } .animateOnAppear(using: chartData.chartStyle.globalAnimation) { diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index 6cf1f01b..bae79bff 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -35,23 +35,23 @@ internal struct LegendView: View where T: CTChartData { case .line: line(legend) - .accessibility(label: Text(accessibilityLegendLabel(legend: legend))) - .accessibility(value: Text("\(legend.legend)")) + .accessibilityLabel( Text(accessibilityLegendLabel(legend: legend))) + .accessibilityValue(Text("\(legend.legend)")) case .bar: bar(legend) .if(scaleLegendBar(legend: legend)) { $0.scaleEffect(1.2, anchor: .leading) } - .accessibility(label: Text(accessibilityLegendLabel(legend: legend))) - .accessibility(value: Text("\(legend.legend)")) + .accessibilityLabel( Text(accessibilityLegendLabel(legend: legend))) + .accessibilityValue(Text("\(legend.legend)")) case .pie: pie(legend) .if(scaleLegendPie(legend: legend)) { $0.scaleEffect(1.2, anchor: .leading) } - .accessibility(label: Text(accessibilityLegendLabel(legend: legend))) - .accessibility(value: Text("\(legend.legend)")) + .accessibilityLabel( Text(accessibilityLegendLabel(legend: legend))) + .accessibilityValue(Text("\(legend.legend)")) } } }.id(UUID()) diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift index 24e9848d..86d82012 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift @@ -49,8 +49,8 @@ internal struct YAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol .foregroundColor(chartData.chartStyle.yAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibility(label: Text("Y Axis Label")) - .accessibility(value: Text("\(labelsArray[i], specifier: specifier)")) + .accessibilityLabel( Text("Y Axis Label")) + .accessibilityValue(Text("\(labelsArray[i], specifier: specifier)")) if i != 0 { Spacer() .frame(minHeight: 0, maxHeight: 500) diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index 74292c42..429084e7 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -119,8 +119,8 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { $0.position(x: geo.size.width + 18, y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo.frame(in: .local))) }) - .accessibility(label: Text("P O I Marker")) - .accessibility(value: Text("\(markerName), \(markerValue, specifier: specifier)")) + .accessibilityLabel( Text("P O I Marker")) + .accessibilityValue(Text("\(markerName), \(markerValue, specifier: specifier)")) case .center(specifier: let specifier): @@ -136,8 +136,8 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { .position(x: geo.size.width / 2, y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo.frame(in: .local))) .opacity(startAnimation ? 1 : 0) - .accessibility(label: Text("P O I Marker")) - .accessibility(value: Text("\(markerName), \(markerValue, specifier: specifier)")) + .accessibilityLabel( Text("P O I Marker")) + .accessibilityValue(Text("\(markerName), \(markerValue, specifier: specifier)")) } } } From ba526a82315a4025c66ef5807f8122d67ecc4e32 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 28 Feb 2021 09:08:14 +0000 Subject: [PATCH 101/152] Move getYLabels to an extension. --- .../BarChart/Models/ChartData/BarChartData.swift | 10 +--------- .../Models/ChartData/GroupedBarChartData.swift | 9 --------- .../Models/ChartData/StackedBarChartData.swift | 9 --------- .../LineChart/Models/ChartData/LineChartData.swift | 12 ------------ .../Models/ChartData/MultiLineChartData.swift | 13 ------------- 5 files changed, 1 insertion(+), 52 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 29a4a229..956e138b 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -130,15 +130,7 @@ public final class BarChartData: CTBarChartDataProtocol { } } } - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let maxValue: Double = self.maxValue - for index in 0...self.chartStyle.yAxisNumberOfLabels { - labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) - } - return labels - } - + // MARK: Touch public func setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) { self.infoView.isTouchCurrent = true diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 109eb0d4..22b34669 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -176,15 +176,6 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { } } - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let maxValue: Double = self.maxValue - for index in 0...self.chartStyle.yAxisNumberOfLabels { - labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) - } - return labels - } - // MARK: Touch public func setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) { self.infoView.isTouchCurrent = true diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index b536f490..4b254b85 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -163,15 +163,6 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { } } - public func getYLabels() -> [Double] { - var labels : [Double] = [Double]() - let maxValue: Double = self.maxValue - for index in 0...self.chartStyle.yAxisNumberOfLabels { - labels.append(maxValue / Double(self.chartStyle.yAxisNumberOfLabels) * Double(index)) - } - return labels - } - // MARK: Touch public func setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) { self.infoView.isTouchCurrent = true diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index 6d7c5419..1f23ffde 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -135,18 +135,6 @@ public final class LineChartData: CTLineChartDataProtocol { } } } - - 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) - labels.append(minValue) - for index in 1...self.chartStyle.yAxisNumberOfLabels { - labels.append(minValue + range * Double(index)) - } - return labels - } // MARK: Points public func getPointMarker() -> some View { diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index bf166335..bbe1495c 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -143,19 +143,6 @@ public final class MultiLineChartData: CTLineChartDataProtocol { } } - 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) - - labels.append(minValue) - for index in 1...self.chartStyle.yAxisNumberOfLabels { - labels.append(minValue + range * Double(index)) - } - return labels - } - // MARK: Points public func getPointMarker() -> some View { ForEach(self.dataSets.dataSets, id: \.self) { dataSet in From 7f5ac6355618c38b477fa0cda18b695b2d060671 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 28 Feb 2021 09:10:50 +0000 Subject: [PATCH 102/152] Add topLine and Base line functionality. --- .../BarChart/Models/Style/BarChartStyle.swift | 16 ++- .../Models/Protocols/LineChartProtocols.swift | 7 +- .../Models/Style/LineChartStyle.swift | 8 ++ .../Extras/LineAndBarEnums.swift | 15 +++ .../Protocols/LineAndBarProtocols.swift | 10 ++ .../LineAndBarProtocolsExtentions.swift | 116 ++++++++++++------ 6 files changed, 127 insertions(+), 45 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift index 3bc0ef5c..2de9b3aa 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift @@ -43,6 +43,9 @@ public struct BarChartStyle: CTBarChartStyle { public var yAxisLabelColour : Color public var yAxisNumberOfLabels : Int + public var baseline : Baseline + public var topLine : Topline + public var globalAnimation : Animation /// Model for controlling the overall aesthetic of the Bar Chart. @@ -64,12 +67,15 @@ public struct BarChartStyle: CTBarChartStyle { /// - yAxisNumberOfLabel: Number Of Labels on Y Axis. /// - yAxisLabelColour: Text Colour for the labels on the Y 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, - markerType : BarMarkerType = .full, + markerType : BarMarkerType = .full, xAxisGridStyle : GridStyle = GridStyle(), xAxisLabelPosition : XAxisLabelPosistion = .bottom, @@ -81,13 +87,16 @@ public struct BarChartStyle: CTBarChartStyle { yAxisLabelColour : Color = Color.primary, yAxisNumberOfLabels : Int = 10, + baseline : Baseline = .minimumValue, + topLine : Topline = .maximumValue, + globalAnimation : Animation = Animation.linear(duration: 1) ) { self.infoBoxPlacement = infoBoxPlacement self.infoBoxValueColour = infoBoxValueColour self.infoBoxDescriptionColour = infoBoxDescriptionColour - self.markerType = markerType + self.markerType = markerType self.xAxisGridStyle = xAxisGridStyle self.xAxisLabelPosition = xAxisLabelPosition @@ -99,6 +108,9 @@ public struct BarChartStyle: CTBarChartStyle { self.yAxisNumberOfLabels = yAxisNumberOfLabels self.yAxisLabelColour = yAxisLabelColour + self.baseline = baseline + self.topLine = topLine + self.globalAnimation = globalAnimation } } diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index 835a31d1..0a3b2048 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -58,12 +58,7 @@ public protocol CTLineChartDataProtocol: CTLineBarChartDataProtocol { /** A protocol to extend functionality of `CTLineBarChartStyle` specifically for Line Charts. */ -public protocol CTLineChartStyle : CTLineBarChartStyle { - /** - Where to start drawing the line chart from. Zero or data set minium. - */ - var baseline: Baseline { get set } -} +public protocol CTLineChartStyle : CTLineBarChartStyle {} diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift index 6bda153f..18a74e86 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift @@ -48,6 +48,8 @@ public struct LineChartStyle: CTLineChartStyle { public var yAxisNumberOfLabels : Int public var baseline : Baseline + public var topLine : Topline + public var globalAnimation : Animation /// Model for controlling the overall aesthetic of the chart. @@ -69,6 +71,8 @@ public struct LineChartStyle: CTLineChartStyle { /// - yAxisNumberOfLabel: Number Of Labels on Y 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, @@ -87,6 +91,8 @@ public struct LineChartStyle: CTLineChartStyle { yAxisNumberOfLabels : Int = 10, baseline : Baseline = .minimumValue, + topLine : Topline = .maximumValue, + globalAnimation : Animation = Animation.linear(duration: 1) ) { self.infoBoxPlacement = infoBoxPlacement @@ -106,6 +112,8 @@ public struct LineChartStyle: CTLineChartStyle { self.yAxisLabelColour = yAxisLabelColour self.baseline = baseline + self.topLine = topLine + self.globalAnimation = globalAnimation } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift b/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift index f0e95363..d0a5c07d 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift @@ -68,3 +68,18 @@ public enum DisplayValue { /// Places the label in the center of chart. case center(specifier: String) } + + +/** + 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/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index a37b42d3..c16292f2 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -128,6 +128,16 @@ public protocol CTLineBarChartStyle: CTChartStyle { */ var yAxisNumberOfLabels: Int { 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 diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift index 80d496e5..7d86e676 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift @@ -7,46 +7,30 @@ import Foundation +// MARK: - Single Data Set extension CTLineBarChartDataProtocol where Set: CTSingleDataSetProtocol { public var range : Double { - return DataFunctions.dataSetRange(from: dataSets) - } - public var minValue : Double { - return DataFunctions.dataSetMinValue(from: dataSets) - } - public var maxValue : Double { - return DataFunctions.dataSetMaxValue(from: dataSets) - } - public var average : Double { - return DataFunctions.dataSetAverage(from: dataSets) - } -} - -extension CTLineBarChartDataProtocol where Set: CTMultiDataSetProtocol { - public var range : Double { - return DataFunctions.multiDataSetRange(from: dataSets) - } - public var minValue : Double { - return DataFunctions.multiDataSetMinValue(from: dataSets) - } - public var maxValue : Double { - return DataFunctions.multiDataSetMaxValue(from: dataSets) - } - public var average : Double { - return DataFunctions.multiDataSetAverage(from: dataSets) - } -} - -extension CTLineBarChartDataProtocol where Self: LineChartData { - public var range : Double { + + var _lowestValue : Double + var _highestValue : Double + switch self.chartStyle.baseline { case .minimumValue: - return DataFunctions.dataSetRange(from: dataSets) + _lowestValue = DataFunctions.dataSetMinValue(from: dataSets) case .minimumWithMaximum(of: let value): - return DataFunctions.dataSetMaxValue(from: dataSets) - min(DataFunctions.dataSetMinValue(from: dataSets), value) + _lowestValue = min(DataFunctions.dataSetMinValue(from: dataSets), value) case .zero: - return DataFunctions.dataSetMaxValue(from: dataSets) + _lowestValue = 0 + } + + switch self.chartStyle.topLine { + case .maximumValue: + _highestValue = DataFunctions.dataSetMaxValue(from: dataSets) + case .maximum(of: let value): + _highestValue = max(DataFunctions.dataSetMaxValue(from: dataSets), value) } + + return _highestValue - _lowestValue } public var minValue : Double { switch self.chartStyle.baseline { @@ -58,17 +42,46 @@ extension CTLineBarChartDataProtocol where Self: LineChartData { return 0 } } + + public var maxValue : Double { + switch self.chartStyle.topLine { + case .maximumValue: + return DataFunctions.dataSetMaxValue(from: dataSets) + case .maximum(of: let value): + return max(DataFunctions.dataSetMaxValue(from: dataSets), value) + } + } + + public var average : Double { + return DataFunctions.dataSetAverage(from: dataSets) + } } -extension CTLineBarChartDataProtocol where Self: MultiLineChartData { + + +// MARK: - Multi Data Set +extension CTLineBarChartDataProtocol where Set: CTMultiDataSetProtocol { public var range : Double { + + var _lowestValue : Double + var _highestValue : Double + switch self.chartStyle.baseline { case .minimumValue: - return DataFunctions.multiDataSetRange(from: dataSets) + _lowestValue = DataFunctions.multiDataSetMinValue(from: dataSets) case .minimumWithMaximum(of: let value): - return DataFunctions.multiDataSetMaxValue(from: dataSets) - min(DataFunctions.multiDataSetMinValue(from: dataSets), value) + _lowestValue = min(DataFunctions.multiDataSetMinValue(from: dataSets), value) case .zero: - return DataFunctions.multiDataSetMaxValue(from: dataSets) + _lowestValue = 0 + } + + switch self.chartStyle.topLine { + case .maximumValue: + _highestValue = DataFunctions.multiDataSetMaxValue(from: dataSets) + case .maximum(of: let value): + _highestValue = max(DataFunctions.multiDataSetMaxValue(from: dataSets), value) } + + return _highestValue - _lowestValue } public var minValue : Double { switch self.chartStyle.baseline { @@ -80,4 +93,33 @@ extension CTLineBarChartDataProtocol where Self: MultiLineChartData { return 0 } } + + public var maxValue : Double { + switch self.chartStyle.topLine { + case .maximumValue: + return DataFunctions.multiDataSetMaxValue(from: dataSets) + case .maximum(of: let value): + return max(DataFunctions.multiDataSetMaxValue(from: dataSets), value) + } + } + + public var average : Double { + return DataFunctions.multiDataSetAverage(from: dataSets) + } +} + +// 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 + } } From 0e659f0b0c5b2cfe6a74657bec7043ef51a4601b Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 28 Feb 2021 09:11:25 +0000 Subject: [PATCH 103/152] Fix count. --- .../ViewModifiers/XAxisGrid.swift | 2 +- .../ViewModifiers/YAxisGrid.swift | 2 +- .../ViewModifiers/YAxisLabels.swift | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift index 621781e7..d7cc5772 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift @@ -18,7 +18,7 @@ internal struct XAxisGrid: ViewModifier where T: CTLineBarChartDataProtocol { ZStack { if chartData.isGreaterThanTwo() { HStack { - ForEach((0...chartData.chartStyle.xAxisGridStyle.numberOfLines), id: \.self) { index in + ForEach((0...chartData.chartStyle.xAxisGridStyle.numberOfLines-1), id: \.self) { index in if index != 0 { VerticalGridView(chartData: chartData) Spacer() diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift index 6868c6f7..5d8d8202 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift @@ -18,7 +18,7 @@ internal struct YAxisGrid: ViewModifier where T: CTLineBarChartDataProtocol { ZStack { if chartData.isGreaterThanTwo() { VStack { - ForEach((0...chartData.chartStyle.yAxisGridStyle.numberOfLines), id: \.self) { index in + ForEach((0...chartData.chartStyle.yAxisGridStyle.numberOfLines-1), id: \.self) { index in if index != 0 { HorizontalGridView(chartData: chartData) Spacer() diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift index 86d82012..b26e6958 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift @@ -13,12 +13,12 @@ import SwiftUI 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 + + private let specifier : String + private var labelsArray : [Double] { chartData.getYLabels() } + + private let labelsAndTop : Bool + private let labelsAndBottom : Bool internal init(chartData: T, specifier: String @@ -43,13 +43,13 @@ internal struct YAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol if labelsAndTop { textAsSpacer } - ForEach((0...chartData.chartStyle.yAxisNumberOfLabels).reversed(), id: \.self) { i in + ForEach((0...chartData.chartStyle.yAxisNumberOfLabels-1).reversed(), id: \.self) { i in Text("\(labelsArray[i], specifier: specifier)") .font(.caption) .foregroundColor(chartData.chartStyle.yAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibilityLabel( Text("Y Axis Label")) + .accessibilityLabel(Text("Y Axis Label")) .accessibilityValue(Text("\(labelsArray[i], specifier: specifier)")) if i != 0 { Spacer() @@ -74,7 +74,7 @@ internal struct YAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol ) } - internal func body(content: Content) -> some View { + internal func body(content: Content) -> some View { Group { if chartData.isGreaterThanTwo() { switch chartData.chartStyle.yAxisLabelPosition { From ced9dacc7a7343311c757eb32ca3656ea270860f Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 28 Feb 2021 09:11:59 +0000 Subject: [PATCH 104/152] Tidy up. --- .../SwiftUICharts/BarChart/Views/SubViews/Bars.swift | 5 ++--- Sources/SwiftUICharts/Shared/Models/InfoViewData.swift | 10 ---------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift index 693a4d8f..9640b091 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift @@ -59,7 +59,6 @@ internal struct ColourBar: View { .animateOnDisappear(using: chartStyle.globalAnimation) { self.startAnimation = false } - .accessibilityValue(Text("\(data.value, specifier: specifier), \(data.pointDescription ?? "")")) } } @@ -123,7 +122,7 @@ internal struct GradientColoursBar: View { .animateOnDisappear(using: chartStyle.globalAnimation) { self.startAnimation = false } - .accessibilityValue(Text("\(data.value, specifier: "%.f") \(data.pointDescription ?? "")")) + .accessibilityValue(Text("\(data.value, specifier: specifier) \(data.pointDescription ?? "")")) } } @@ -186,7 +185,7 @@ internal struct GradientStopsBar: View { .animateOnDisappear(using: chartStyle.globalAnimation) { self.startAnimation = false } - .accessibilityValue(Text("\(data.value, specifier: "%.f") \(data.pointDescription ?? "")")) + .accessibilityValue(Text("\(data.value, specifier: specifier) \(data.pointDescription ?? "")")) } } diff --git a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift index c48518a1..f3e9df49 100644 --- a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift @@ -30,16 +30,6 @@ public struct InfoViewData { */ var touchOverlayInfo : [DP] = [] -// var accessibilityLabels : String { -// get { -// var _returnData = "" -// for info in touchOverlayInfo { -// _returnData += String(format: touchSpecifier, info.value) + "\(info.pointDescription ?? "")," -// } -// return _returnData -// } -// } - /** Set specifier of data point readout. From 6ace4577ad39b82d5b1281aeb664e2d221da7daa Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 28 Feb 2021 09:12:49 +0000 Subject: [PATCH 105/152] Refactor YAxisPOI to make the compiler happy. --- .../SharedLineAndBar/Shapes/LabelShape.swift | 20 ++++- .../ViewModifiers/YAxisPOI.swift | 47 ++++------- .../Views/ValueLabelCenterSubView.swift | 77 +++++++++++++++++++ .../Views/ValueLabelYAxisSubView.swift | 77 +++++++++++++++++++ 4 files changed, 189 insertions(+), 32 deletions(-) create mode 100644 Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelCenterSubView.swift create mode 100644 Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelYAxisSubView.swift diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/LabelShape.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/LabelShape.swift index 711001cc..3addfdb2 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/LabelShape.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/LabelShape.swift @@ -8,9 +8,9 @@ import SwiftUI /** - Shape used in POI Markers when displaying value in the Y axid labels. + Shape used in POI Markers when displaying value in the Y axis labels on the leading edge. */ -public struct LabelShape: Shape { +public struct LeadingLabelShape: Shape { public func path(in rect: CGRect) -> Path { var path = Path() path.move(to: CGPoint(x: rect.minX, y: rect.maxY)) @@ -22,3 +22,19 @@ public struct LabelShape: Shape { 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/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index 429084e7..f5774e6c 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -103,46 +103,33 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { case .yAxis(specifier: let specifier): - Text("\(markerValue, specifier: specifier)") - .font(.caption) - .foregroundColor(labelColour) - .padding(4) - .background(Color.blue) - .clipShape(LabelShape()) - .overlay(LabelShape() - .stroke(lineColour) - ) - .ifElse(self.chartData.chartStyle.yAxisLabelPosition == .leading, if: { - $0.position(x: -18, - y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo.frame(in: .local))) - }, else: { - $0.position(x: geo.size.width + 18, - y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo.frame(in: .local))) - }) + ValueLabelYAxisSubView(chartData : chartData, + markerValue : markerValue, + specifier : specifier, + labelColour : labelColour, + labelBackground: labelBackground, + lineColour : lineColour, + chartSize : geo.frame(in: .local)) .accessibilityLabel( Text("P O I Marker")) .accessibilityValue(Text("\(markerName), \(markerValue, specifier: specifier)")) case .center(specifier: let specifier): - Text("\(markerValue, specifier: specifier)") - .font(.caption) - .foregroundColor(labelColour) - .padding() - .background(labelBackground) - .clipShape(DiamondShape()) - .overlay(DiamondShape() - .stroke(lineColour, style: strokeStyle) - ) - .position(x: geo.size.width / 2, - y: getYPoint(chartType: chartData.chartType.chartType, chartSize: geo.frame(in: .local))) - .opacity(startAnimation ? 1 : 0) - .accessibilityLabel( Text("P O I Marker")) + ValueLabelCenterSubView(chartData : chartData, + markerValue : markerValue, + specifier : specifier, + labelColour : labelColour, + labelBackground : labelBackground, + lineColour : lineColour, + strokeStyle : strokeStyle, + chartSize : geo.frame(in: .local)) + .accessibilityLabel(Text("P O I Marker")) .accessibilityValue(Text("\(markerName), \(markerValue, specifier: specifier)")) } } } - func getYPoint(chartType: ChartType, chartSize: CGRect) -> CGFloat { + private func getYPoint(chartType: ChartType, chartSize: CGRect) -> CGFloat { switch chartData.chartType.chartType { case .line: let y = chartSize.height / CGFloat(range) diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelCenterSubView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelCenterSubView.swift new file mode 100644 index 00000000..64d7e0c0 --- /dev/null +++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelCenterSubView.swift @@ -0,0 +1,77 @@ +// +// 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 + private let chartSize : CGRect + + internal init(chartData : T, + markerValue : Double, + specifier : String, + labelColour : Color, + labelBackground : Color, + lineColour : Color, + strokeStyle : StrokeStyle, + chartSize : CGRect + ) { + self.chartData = chartData + self.markerValue = markerValue + self.specifier = specifier + self.labelColour = labelColour + self.labelBackground = labelBackground + self.lineColour = lineColour + self.strokeStyle = strokeStyle + self.chartSize = chartSize + } + + @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) + ) + .position(x: chartSize.width / 2, + y: getYPoint(chartType: chartData.chartType.chartType, chartSize: chartSize)) + .opacity(startAnimation ? 1 : 0) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = false + } + + } + + private func getYPoint(chartType: ChartType, chartSize: CGRect) -> CGFloat { + switch chartData.chartType.chartType { + case .line: + let y = chartSize.height / CGFloat(chartData.range) + return (CGFloat(markerValue - chartData.minValue) * -y) + chartSize.height + case .bar: + let y = chartSize.height / CGFloat(chartData.maxValue) + return chartSize.height - CGFloat(markerValue) * y + case .pie: + return 0 + } + } +} + diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelYAxisSubView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelYAxisSubView.swift new file mode 100644 index 00000000..85cc230a --- /dev/null +++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelYAxisSubView.swift @@ -0,0 +1,77 @@ +// +// 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 + private let chartSize : CGRect + + internal init(chartData : T, + markerValue : Double, + specifier : String, + labelColour : Color, + labelBackground : Color, + lineColour : Color, + chartSize : CGRect + ) { + self.chartData = chartData + self.markerValue = markerValue + self.specifier = specifier + self.labelColour = labelColour + self.labelBackground = labelBackground + self.lineColour = lineColour + self.chartSize = chartSize + } + + 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) + ) + + .position(x: -(chartData.infoView.yAxisLabelWidth / 2) - 6, + y: getYPoint(chartType: chartData.chartType.chartType, chartSize: chartSize)) + }, else: { + $0 + .clipShape(TrailingLabelShape()) + .overlay(TrailingLabelShape() + .stroke(lineColour) + ) + .position(x: chartSize.width + (chartData.infoView.yAxisLabelWidth / 2), + y: getYPoint(chartType: chartData.chartType.chartType, chartSize: chartSize)) + }) + + } + + private func getYPoint(chartType: ChartType, chartSize: CGRect) -> CGFloat { + switch chartData.chartType.chartType { + case .line: + let y = chartSize.height / CGFloat(chartData.range) + return (CGFloat(markerValue - chartData.minValue) * -y) + chartSize.height + case .bar: + let y = chartSize.height / CGFloat(chartData.maxValue) + return chartSize.height - CGFloat(markerValue) * y + case .pie: + return 0 + } + } +} From c3cbe52f9e9e38030323c1a7a2e39b6b66f714fa Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 28 Feb 2021 09:30:50 +0000 Subject: [PATCH 106/152] Move setTouchInteraction to a protocol extension. --- .../Models/ChartData/BarChartData.swift | 12 +++--------- .../Models/ChartData/GroupedBarChartData.swift | 12 ++---------- .../Models/ChartData/StackedBarChartData.swift | 12 ++---------- .../LineChart/Extras/LineChartEnums.swift | 17 ----------------- .../Models/ChartData/LineChartData.swift | 10 ---------- .../Models/ChartData/MultiLineChartData.swift | 12 ++---------- .../Models/ChartData/DoughnutChartData.swift | 13 ++++--------- .../ChartData/MultiLayerPieChartData.swift | 13 ++++--------- .../Models/ChartData/PieChartData.swift | 13 ++++--------- .../Protocols/PieChartProtocolsExtentions.swift | 6 ------ .../Models/Protocols/SharedProtocols.swift | 14 +++++++++++++- .../Protocols/SharedProtocolsExtensions.swift | 11 ++++++++++- .../Extras/LineAndBarEnums.swift | 16 ++++++++++++++++ 13 files changed, 60 insertions(+), 101 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 956e138b..f8c35ca9 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -131,13 +131,7 @@ public final class BarChartData: CTBarChartDataProtocol { } } - // MARK: Touch - 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) - } + @ViewBuilder public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { @@ -181,7 +175,7 @@ public final class BarChartData: CTBarChartDataProtocol { // MARK: - Touch extension BarChartData: TouchProtocol { - internal func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + public 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) @@ -191,7 +185,7 @@ extension BarChartData: TouchProtocol { self.infoView.touchOverlayInfo = points } - internal func getPointLocation(dataSet: BarDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { + public 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) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 22b34669..5de906cd 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -175,14 +175,6 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { } } } - - // MARK: Touch - 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) - } @ViewBuilder public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { @@ -227,7 +219,7 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { // MARK: - Touch extension GroupedBarChartData: TouchProtocol { - internal func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [MultiBarChartDataPoint] = [] @@ -253,7 +245,7 @@ extension GroupedBarChartData: TouchProtocol { self.infoView.touchOverlayInfo = points } - internal func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { + public func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { // Divide the chart into equal sections. let superXSection : CGFloat = (chartSize.width / CGFloat(dataSet.dataSets.count)) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index 4b254b85..335159db 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -162,14 +162,6 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { } } } - - // MARK: Touch - 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) - } @ViewBuilder public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { @@ -213,7 +205,7 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { // MARK: - Touch extension StackedBarChartData: TouchProtocol { - internal func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [MultiBarChartDataPoint] = [] @@ -259,7 +251,7 @@ extension StackedBarChartData: TouchProtocol { self.infoView.touchOverlayInfo = points } - internal func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { + public 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) diff --git a/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift b/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift index 8244ac19..24cff73d 100644 --- a/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift +++ b/Sources/SwiftUICharts/LineChart/Extras/LineChartEnums.swift @@ -21,23 +21,6 @@ public enum LineType { case curvedLine } -/** - 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 -} - /** Style of the point marks ``` diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index 1f23ffde..69d007ed 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -144,16 +144,6 @@ public final class LineChartData: CTLineChartDataProtocol { animation : self.chartStyle.globalAnimation, isFilled : self.isFilled) } - - // MARK: Touch - 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) - } - - public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { self.markerSubView(dataSet: self.dataSets, touchLocation: touchLocation, chartSize: chartSize) diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index bbe1495c..d2990fdc 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -153,15 +153,7 @@ public final class MultiLineChartData: CTLineChartDataProtocol { isFilled : self.isFilled) } } - - // MARK: Touch - 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) - } - + public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { ZStack { ForEach(self.dataSets.dataSets, id: \.self) { dataSet in @@ -199,7 +191,7 @@ public final class MultiLineChartData: CTLineChartDataProtocol { // MARK: - Touch extension MultiLineChartData: TouchProtocol { - internal func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + 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) diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift index f46585ee..3560fdcf 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift @@ -67,14 +67,6 @@ public final class DoughnutChartData: CTDoughnutChartDataProtocol { self.makeDataPoints() } - // MARK: Touch - 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) - } - public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { EmptyView() } public typealias Set = PieDataSet @@ -84,7 +76,7 @@ public final class DoughnutChartData: CTDoughnutChartDataProtocol { // MARK: - Touch extension DoughnutChartData: TouchProtocol { - internal func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [PieChartDataPoint] = [] let touchDegree = degree(from: touchLocation, in: chartSize) @@ -94,6 +86,9 @@ extension DoughnutChartData: TouchProtocol { } self.infoView.touchOverlayInfo = points } + public func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { + return nil + } } // MARK: - Legends diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift index bb8a6b64..751de7a7 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift @@ -120,14 +120,6 @@ public final class MultiLayerPieChartData: CTMultiPieChartDataProtocol { self.makeDataPoints() } - // MARK: Touch - 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) - } - public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { EmptyView() } public typealias Set = MultiPieDataSet @@ -137,10 +129,13 @@ public final class MultiLayerPieChartData: CTMultiPieChartDataProtocol { // MARK: - Touch extension MultiLayerPieChartData: TouchProtocol { - internal func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { let points : [MultiPieDataPoint] = [] self.infoView.touchOverlayInfo = points } + public func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { + return nil + } } // MARK: - Legends diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift index d74a63e3..086fd7d6 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift @@ -68,14 +68,6 @@ public final class PieChartData: CTPieChartDataProtocol { self.makeDataPoints() } - // MARK: Touch - 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) - } - public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { EmptyView() } public typealias Set = PieDataSet @@ -85,7 +77,7 @@ public final class PieChartData: CTPieChartDataProtocol { // MARK: - Touch extension PieChartData: TouchProtocol { - internal func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [PieChartDataPoint] = [] let touchDegree = degree(from: touchLocation, in: chartSize) @@ -95,6 +87,9 @@ extension PieChartData: TouchProtocol { } self.infoView.touchOverlayInfo = points } + public func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { + return nil + } } // MARK: - Legends diff --git a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift index 58c7c764..b31630fc 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift @@ -69,12 +69,6 @@ extension CTPieDoughnutChartDataProtocol where Set == MultiPieDataSet, DataPoint } } -extension CTPieDoughnutChartDataProtocol { - internal func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { - return nil - } -} - extension CTPieDoughnutChartDataProtocol where Set == PieDataSet, DataPoint == PieChartDataPoint { /** diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 68605e12..8d3d342b 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -79,10 +79,22 @@ public protocol CTChartData: ObservableObject, Identifiable { 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. @@ -95,7 +107,7 @@ public protocol CTChartData: ObservableObject, Identifiable { // MARK: - Touch Protocol -internal protocol TouchProtocol { +public protocol TouchProtocol { /// A type representing a data set. -- `CTDataSetProtocol` associatedtype SetPoint : CTDataSetProtocol diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift index 1a025a5e..bf3db640 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift @@ -5,7 +5,7 @@ // Created by Will Dale on 13/02/2021. // -import Foundation +import SwiftUI extension CTChartData where Set: CTSingleDataSetProtocol { public func isGreaterThanTwo() -> Bool { @@ -22,3 +22,12 @@ extension CTChartData where Set: CTMultiDataSetProtocol { return returnValue } } +// MARK: Touch +extension CTChartData where Self: TouchProtocol { + 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) + } +} diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift b/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift index d0a5c07d..56d5c838 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift @@ -69,6 +69,22 @@ public enum DisplayValue { 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. From d6b22666d3f1272e04992febdcc85c5ce07791b3 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 28 Feb 2021 12:53:16 +0000 Subject: [PATCH 107/152] Add touchUnit. --- .../Shared/Extras/SharedEnums.swift | 18 +++++++++ .../Shared/Models/InfoViewData.swift | 17 +++++--- .../Shared/ViewModifiers/HeaderBox.swift | 33 ++++++++++----- .../Shared/ViewModifiers/InfoBox.swift | 5 +-- .../Shared/ViewModifiers/TouchOverlay.swift | 17 ++++---- .../Shared/Views/TouchOverlayBox.swift | 40 ++++++++++++++++--- 6 files changed, 99 insertions(+), 31 deletions(-) diff --git a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift index 8819f8a5..940798ac 100644 --- a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift @@ -72,3 +72,21 @@ public enum InfoBoxPlacement { /// Fix 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 Unit { + /// No units + case none + /// Before value + case prefix(of: String) + /// After value + case suffix(of: String) +} diff --git a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift index f3e9df49..b6b85a0d 100644 --- a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift @@ -19,7 +19,7 @@ public struct InfoViewData { Used by `HeaderBox` and `InfoBox`. */ - var isTouchCurrent : Bool = false + var isTouchCurrent: Bool = false /** Closest data points to input. @@ -28,7 +28,7 @@ public struct InfoViewData { Used by `HeaderBox` and `InfoBox`. */ - var touchOverlayInfo : [DP] = [] + var touchOverlayInfo: [DP] = [] /** Set specifier of data point readout. @@ -37,7 +37,7 @@ public struct InfoViewData { Used by `HeaderBox` and `InfoBox`. */ - var touchSpecifier : String = "%.0f" + var touchSpecifier: String = "%.0f" /** X axis posistion of the overlay box. @@ -48,7 +48,7 @@ public struct InfoViewData { Used by `HeaderBox` and `InfoBox`. */ - var touchLocation : CGPoint = .zero + var touchLocation: CGPoint = .zero /** Size of the chart. @@ -59,7 +59,7 @@ public struct InfoViewData { Used by `HeaderBox` and `InfoBox`. */ - var chartSize : CGRect = .zero + var chartSize: CGRect = .zero /** Current width of the `YAxisLabels` @@ -67,5 +67,10 @@ public struct InfoViewData { Needed line up the touch overlay to compensate for the loss of width. */ - var yAxisLabelWidth : CGFloat = 0 + var yAxisLabelWidth: CGFloat = 0 + + /** + Option to display units before or after values. + */ + var touchUnit: Unit = .none } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index 774e28e4..6b069e63 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -34,12 +34,30 @@ internal struct HeaderBox: ViewModifier where T: CTChartData { VStack(alignment: .trailing) { if chartData.infoView.isTouchCurrent { ForEach(chartData.infoView.touchOverlayInfo, id: \.self) { info in - Text("\(info.value, specifier: chartData.infoView.touchSpecifier)") - .font(.title3) - .foregroundColor(chartData.chartStyle.infoBoxValueColour) - Text("\(info.pointDescription ?? "")") - .font(.subheadline) - .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) + switch chartData.infoView.touchUnit { + case .none: + Text("\(info.value, specifier: chartData.infoView.touchSpecifier)") + .font(.title3) + .foregroundColor(chartData.chartStyle.infoBoxValueColour) + Text("\(info.pointDescription ?? "")") + .font(.subheadline) + .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) + case .prefix(of: let unit): + Text("\(unit) \(info.value, specifier: chartData.infoView.touchSpecifier)") + .font(.title3) + .foregroundColor(chartData.chartStyle.infoBoxValueColour) + Text("\(info.pointDescription ?? "")") + .font(.subheadline) + .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) + case .suffix(of: let unit): + Text("\(info.value, specifier: chartData.infoView.touchSpecifier) \(unit)") + .font(.title3) + .foregroundColor(chartData.chartStyle.infoBoxValueColour) + Text("\(info.pointDescription ?? "")") + .font(.subheadline) + .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) + } + } } else { Text("") @@ -82,9 +100,6 @@ internal struct HeaderBox: ViewModifier where T: CTChartData { .frame(minWidth: 0, maxWidth: .infinity) } content -// .onChange(of: chartData.infoView.accessibilityLabels) { (value) in -// Accessibility.read(this: value) -// } } } } else { content } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift index d240b7f7..24ddb6dd 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift @@ -27,9 +27,6 @@ internal struct InfoBox: ViewModifier where T: CTChartData { EmptyView() } content -// .onChange(of: chartData.infoView.accessibilityLabels) { (value) in -// Accessibility.read(this: value) -// } } } @@ -37,6 +34,7 @@ internal struct InfoBox: ViewModifier where T: CTChartData { TouchOverlayBox(isTouchCurrent : chartData.infoView.isTouchCurrent, selectedPoints : chartData.infoView.touchOverlayInfo, specifier : chartData.infoView.touchSpecifier, + unit : chartData.infoView.touchUnit, valueColour : chartData.chartStyle.infoBoxValueColour, descriptionColour: chartData.chartStyle.infoBoxDescriptionColour, boxFrame : $boxFrame) @@ -53,6 +51,7 @@ internal struct InfoBox: ViewModifier where T: CTChartData { TouchOverlayBox(isTouchCurrent : chartData.infoView.isTouchCurrent, selectedPoints : chartData.infoView.touchOverlayInfo, specifier : chartData.infoView.touchSpecifier, + unit : chartData.infoView.touchUnit, valueColour : chartData.chartStyle.infoBoxValueColour, descriptionColour: chartData.chartStyle.infoBoxDescriptionColour, boxFrame : $boxFrame) diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 3c78739f..f3dd0061 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -15,11 +15,13 @@ internal struct TouchOverlay: ViewModifier where T: CTChartData { @ObservedObject var chartData: T - internal init(chartData : T, - specifier : String + internal init(chartData : T, + specifier : String, + unit : Unit ) { self.chartData = chartData self.chartData.infoView.touchSpecifier = specifier + self.chartData.infoView.touchUnit = unit } internal func body(content: Content) -> some View { @@ -31,10 +33,8 @@ internal struct TouchOverlay: ViewModifier where T: CTChartData { .gesture( DragGesture(minimumDistance: 0) .onChanged { (value) in - chartData.setTouchInteraction(touchLocation: value.location, chartSize: geo.frame(in: .local)) - } .onEnded { _ in chartData.infoView.isTouchCurrent = false @@ -76,10 +76,12 @@ extension View { - Returns: A new view containing the chart with a touch overlay. */ public func touchOverlay(chartData: T, - specifier: String = "%.0f" + specifier: String = "%.0f", + unit : Unit = .none ) -> some View { self.modifier(TouchOverlay(chartData: chartData, - specifier: specifier)) + specifier: specifier, + unit : unit)) } #elseif os(tvOS) /** @@ -89,7 +91,8 @@ extension View { Unavailable in tvOS */ public func touchOverlay(chartData: T, - specifier: String = "%.0f" + specifier: String = "%.0f", + unit : Unit ) -> some View { self.modifier(EmptyModifier()) } diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index ed9a5327..644032b5 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -15,6 +15,7 @@ internal struct TouchOverlayBox: View { private var isTouchCurrent : Bool private var selectedPoints : [D] private var specifier : String + private var unit : Unit private var valueColour : Color private var descriptionColour : Color @@ -26,6 +27,7 @@ internal struct TouchOverlayBox: View { internal init(isTouchCurrent : Bool, selectedPoints : [D], specifier : String = "%.0f", + unit : Unit, valueColour : Color, descriptionColour : Color, boxFrame : Binding, @@ -34,6 +36,7 @@ internal struct TouchOverlayBox: View { self.isTouchCurrent = isTouchCurrent self.selectedPoints = selectedPoints self.specifier = specifier + self.unit = unit self.valueColour = valueColour self.descriptionColour = descriptionColour self._boxFrame = boxFrame @@ -44,14 +47,39 @@ internal struct TouchOverlayBox: View { HStack { ForEach(selectedPoints, id: \.self) { point in - Text("\(point.value, specifier: specifier)") - .font(.subheadline) - .foregroundColor(valueColour) - if let label = point.pointDescription { - Text(label) + + switch unit { + case .none: + Text("\(point.value, specifier: specifier)") .font(.subheadline) - .foregroundColor(descriptionColour) + .foregroundColor(valueColour) + if let label = point.pointDescription { + Text(label) + .font(.subheadline) + .foregroundColor(descriptionColour) + } + case .prefix(of: let unit): + Text("\(unit) \(point.value, specifier: specifier)") + .font(.subheadline) + .foregroundColor(valueColour) + if let label = point.pointDescription { + Text(label) + .font(.subheadline) + .foregroundColor(descriptionColour) + } + case .suffix(of: let unit): + Text("\(point.value, specifier: specifier) \(unit)") + .font(.subheadline) + .foregroundColor(valueColour) + if let label = point.pointDescription { + Text(label) + .font(.subheadline) + .foregroundColor(descriptionColour) + } } + + + } } From d3603b52a3d8266a0efd068b19a34f3bb539ab1d Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 28 Feb 2021 12:53:44 +0000 Subject: [PATCH 108/152] Open metadata out to api. --- .../Shared/Models/ChartMetadata.swift | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift index 4a57c526..a671c129 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift @@ -19,13 +19,13 @@ import SwiftUI */ public struct ChartMetadata { /// The charts title - var title : String + public var title : String /// The charts subtitle - var subtitle : String + public var subtitle : String /// Color of the title - var titleColour : Color + public var titleColour : Color /// Color of the subtitle - var subtitleColour: Color + public var subtitleColour: Color /// Model to hold the metadata for the chart. /// - Parameters: @@ -44,19 +44,3 @@ public struct ChartMetadata { self.subtitleColour = subtitleColour } } -struct Accessibility { - #if os(iOS) - static func read(this value: String) { - if UIAccessibility.isVoiceOverRunning { - UIAccessibility.post(notification: .announcement, argument: "\(value)") - } - } - - #else - - static func read(this value: String) { - - } - - #endif -} From 0c69d035ca9c0cc9edfcb47d65aa1ec0163a1009 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Mon, 1 Mar 2021 08:40:25 +0000 Subject: [PATCH 109/152] Open Legends to API. --- .../BarChart/Models/ChartData/BarChartData.swift | 4 ++-- .../BarChart/Models/ChartData/GroupedBarChartData.swift | 4 ++-- .../BarChart/Models/ChartData/StackedBarChartData.swift | 4 ++-- .../LineChart/Models/ChartData/LineChartData.swift | 6 +++--- .../LineChart/Models/ChartData/MultiLineChartData.swift | 4 ++-- .../PieChart/Models/ChartData/DoughnutChartData.swift | 4 ++-- .../PieChart/Models/ChartData/MultiLayerPieChartData.swift | 4 ++-- .../PieChart/Models/ChartData/PieChartData.swift | 4 ++-- Sources/SwiftUICharts/Shared/Models/InfoViewData.swift | 2 +- .../Shared/Models/Protocols/SharedProtocols.swift | 2 +- Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift | 4 +--- .../Models/Protocols/LineAndBarProtocolsExtentions.swift | 1 + 12 files changed, 21 insertions(+), 22 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index f8c35ca9..402d9cac 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -199,11 +199,11 @@ extension BarChartData: TouchProtocol { // MARK: - Legends extension BarChartData: LegendProtocol { - internal func legendOrder() -> [LegendData] { + public func legendOrder() -> [LegendData] { return legends.sorted { $0.prioity < $1.prioity} } - internal func setupLegends() { + public func setupLegends() { switch self.barStyle.colourFrom { case .barStyle: diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 5de906cd..7ad12f77 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -281,7 +281,7 @@ extension GroupedBarChartData: TouchProtocol { // MARK: - Legends extension GroupedBarChartData: LegendProtocol { - internal func setupLegends() { + public func setupLegends() { for group in self.groups { @@ -320,7 +320,7 @@ extension GroupedBarChartData: LegendProtocol { } } - internal func legendOrder() -> [LegendData] { + public func legendOrder() -> [LegendData] { return legends.sorted { $0.prioity < $1.prioity} } } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index 335159db..bf418812 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -301,7 +301,7 @@ extension StackedBarChartData: TouchProtocol { extension StackedBarChartData: LegendProtocol { // MARK: - Legends - internal func setupLegends() { + public func setupLegends() { for group in self.groups { if group.colourType == .colour, @@ -339,7 +339,7 @@ extension StackedBarChartData: LegendProtocol { } } - internal func legendOrder() -> [LegendData] { + public func legendOrder() -> [LegendData] { return legends.sorted { $0.prioity < $1.prioity} } } diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index 69d007ed..9ca5b91e 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -60,7 +60,7 @@ public final class LineChartData: CTLineChartDataProtocol { public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) - + // MARK: Initializer /// Initialises a Single Line Chart. /// @@ -184,7 +184,7 @@ extension LineChartData: TouchProtocol { // MARK: - Legends extension LineChartData: LegendProtocol { - internal func setupLegends() { + public func setupLegends() { if dataSets.style.colourType == .colour, let colour = dataSets.style.colour @@ -222,7 +222,7 @@ extension LineChartData: LegendProtocol { } } - internal func legendOrder() -> [LegendData] { + public func legendOrder() -> [LegendData] { return legends.sorted { $0.prioity < $1.prioity} } } diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index d2990fdc..04c06ab5 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -207,7 +207,7 @@ extension MultiLineChartData: TouchProtocol { // MARK: - Legends extension MultiLineChartData: LegendProtocol { - internal func setupLegends() { + public func setupLegends() { for dataSet in dataSets.dataSets { if dataSet.style.colourType == .colour, let colour = dataSet.style.colour @@ -246,7 +246,7 @@ extension MultiLineChartData: LegendProtocol { } } - internal func legendOrder() -> [LegendData] { + public func legendOrder() -> [LegendData] { return legends.sorted { $0.prioity < $1.prioity} } } diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift index 3560fdcf..7d7895b0 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift @@ -93,9 +93,9 @@ extension DoughnutChartData: TouchProtocol { // MARK: - Legends extension DoughnutChartData: LegendProtocol { - func setupLegends() {} + public func setupLegends() {} - internal func legendOrder() -> [LegendData] { + public func legendOrder() -> [LegendData] { return legends.sorted { $0.prioity < $1.prioity} } } diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift index 751de7a7..d877acf0 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift @@ -140,9 +140,9 @@ extension MultiLayerPieChartData: TouchProtocol { // MARK: - Legends extension MultiLayerPieChartData: LegendProtocol { - internal func setupLegends() {} + public func setupLegends() {} - internal func legendOrder() -> [LegendData] { + public func legendOrder() -> [LegendData] { return legends.sorted { $0.prioity < $1.prioity} } } diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift index 086fd7d6..97022298 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift @@ -94,7 +94,7 @@ extension PieChartData: TouchProtocol { // MARK: - Legends extension PieChartData: LegendProtocol { - internal func setupLegends() { + public func setupLegends() { for data in dataSets.dataPoints { if let legend = data.pointDescription { self.legends.append(LegendData(id : data.id, @@ -107,7 +107,7 @@ extension PieChartData: LegendProtocol { } } - internal func legendOrder() -> [LegendData] { + public func legendOrder() -> [LegendData] { return legends.sorted { $0.prioity < $1.prioity} } } diff --git a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift index b6b85a0d..2cf1afd3 100644 --- a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift @@ -48,7 +48,7 @@ public struct InfoViewData { Used by `HeaderBox` and `InfoBox`. */ - var touchLocation: CGPoint = .zero + var touchLocation: CGPoint = .zero /** Size of the chart. diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 8d3d342b..3be289bc 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -137,7 +137,7 @@ public protocol TouchProtocol { /** Protocol for dealing with legend data internally. */ -internal protocol LegendProtocol { +public protocol LegendProtocol { /** Sets the order the Legends are layed out in. diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index 644032b5..6163f938 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -77,9 +77,7 @@ internal struct TouchOverlayBox: View { .foregroundColor(descriptionColour) } } - - - + } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift index 7d86e676..c86b673e 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift @@ -32,6 +32,7 @@ extension CTLineBarChartDataProtocol where Set: CTSingleDataSetProtocol { return _highestValue - _lowestValue } + public var minValue : Double { switch self.chartStyle.baseline { case .minimumValue: From e229b3920868795a4af7c43f24c0e2ffa91d6219 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Tue, 2 Mar 2021 13:53:57 +0000 Subject: [PATCH 110/152] Reorganise protocols. Change implementation of colour throughout. --- .../Models/ChartData/BarChartData.swift | 54 +-- .../ChartData/GroupedBarChartData.swift | 28 +- .../ChartData/StackedBarChartData.swift | 28 +- .../Models/Datapoints/BarChartDataPoint.swift | 83 +---- .../Datapoints/MultiBarChartDataPoint.swift | 2 +- .../BarChart/Models/GroupingData.swift | 69 +--- .../Models/Protocols/BarChartProtocols.swift | 27 +- .../BarChart/Models/Style/BarStyle.swift | 87 +---- .../BarChart/Views/GroupedBarChart.swift | 22 +- .../Views/SubViews/BarChartSubViews.swift | 40 +-- .../BarChart/Views/SubViews/Bars.swift | 20 +- .../LineChart/Extras/PathExtensions.swift | 78 ++++- .../Models/ChartData/LineChartData.swift | 55 +++- .../Models/ChartData/MultiLineChartData.swift | 54 ++- .../{ => DataPoints}/LineChartDataPoint.swift | 2 +- .../Models/Protocols/LineChartProtocols.swift | 82 +++-- .../LineChartProtocolsExtensions.swift | 308 +++++++++--------- .../LineChart/Models/Style/LineStyle.swift | 108 +----- .../LineChart/Shapes/LineShape.swift | 42 ++- .../LineChart/Views/FilledLineChart.swift | 60 ++-- .../LineChart/Views/LineChartView.swift | 37 +-- .../LineChart/Views/MultiLineChart.swift | 58 ++-- .../LineChart/Views/RangedLineChart.swift | 134 ++++++++ .../Views/SubViews/LineChartSubViews.swift | 24 +- .../Views/SubViews/PointsSubView.swift | 6 +- .../Models/ChartData/DoughnutChartData.swift | 2 +- .../ChartData/MultiLayerPieChartData.swift | 2 +- .../Models/ChartData/PieChartData.swift | 4 +- .../Models/DataPoints/MultiPieDataPoint.swift | 2 +- .../Models/Protocols/PieChartProtocols.swift | 6 +- .../Shared/Models/ColourStyle.swift | 78 +++++ .../Shared/Models/InfoViewData.swift | 2 +- .../Shared/Models/LegendData.swift | 96 +----- .../Models/Protocols/SharedProtocols.swift | 22 +- .../Protocols/SharedProtocolsExtensions.swift | 2 +- .../Shared/Views/LegendView.swift | 42 +-- .../Shared/Views/TouchOverlayBox.swift | 2 +- .../Protocols/LineAndBarProtocols.swift | 4 +- .../ViewModifiers/YAxisPOI.swift | 4 +- 39 files changed, 902 insertions(+), 874 deletions(-) rename Sources/SwiftUICharts/LineChart/Models/{ => DataPoints}/LineChartDataPoint.swift (95%) create mode 100644 Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift create mode 100644 Sources/SwiftUICharts/Shared/Models/ColourStyle.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 402d9cac..2b0f0914 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -173,7 +173,7 @@ public final class BarChartData: CTBarChartDataProtocol { } // MARK: - Touch -extension BarChartData: TouchProtocol { +extension BarChartData { public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [BarChartDataPoint] = [] @@ -207,34 +207,34 @@ extension BarChartData: LegendProtocol { switch self.barStyle.colourFrom { case .barStyle: - if self.barStyle.colourType == .colour, - let colour = self.barStyle.colour + if self.barStyle.fillColour.colourType == .colour, + let colour = self.barStyle.fillColour.colour { self.legends.append(LegendData(id : dataSets.id, legend : dataSets.legendTitle, - colour : colour, + colour : ColourStyle(colour: colour), strokeStyle: nil, prioity : 1, chartType : .bar)) - } else if self.barStyle.colourType == .gradientColour, - let colours = self.barStyle.colours + } else if self.barStyle.fillColour.colourType == .gradientColour, + let colours = self.barStyle.fillColour.colours { self.legends.append(LegendData(id : dataSets.id, legend : dataSets.legendTitle, - colours : colours, - startPoint : .leading, - endPoint : .trailing, + colour : ColourStyle(colours: colours, + startPoint: .leading, + endPoint: .trailing), strokeStyle: nil, prioity : 1, chartType : .bar)) - } else if self.barStyle.colourType == .gradientStops, - let stops = self.barStyle.stops + } else if self.barStyle.fillColour.colourType == .gradientStops, + let stops = self.barStyle.fillColour.stops { self.legends.append(LegendData(id : dataSets.id, legend : dataSets.legendTitle, - stops : stops, - startPoint : .leading, - endPoint : .trailing, + colour : ColourStyle(stops: stops, + startPoint: .leading, + endPoint: .trailing), strokeStyle: nil, prioity : 1, chartType : .bar)) @@ -243,37 +243,37 @@ extension BarChartData: LegendProtocol { for data in dataSets.dataPoints { - if data.colourType == .colour, - let colour = data.colour, + if data.fillColour.colourType == .colour, + let colour = data.fillColour.colour, let legend = data.pointDescription { self.legends.append(LegendData(id : data.id, legend : legend, - colour : colour, + colour : ColourStyle(colour: colour), strokeStyle: nil, prioity : 1, chartType : .bar)) - } else if data.colourType == .gradientColour, - let colours = data.colours, + } else if data.fillColour.colourType == .gradientColour, + let colours = data.fillColour.colours, let legend = data.pointDescription { self.legends.append(LegendData(id : data.id, legend : legend, - colours : colours, - startPoint : .leading, - endPoint : .trailing, + colour : ColourStyle(colours: colours, + startPoint: .leading, + endPoint: .trailing), strokeStyle: nil, prioity : 1, chartType : .bar)) - } else if data.colourType == .gradientStops, - let stops = data.stops, + } else if data.fillColour.colourType == .gradientStops, + let stops = data.fillColour.stops, let legend = data.pointDescription { self.legends.append(LegendData(id : data.id, legend : legend, - stops : stops, - startPoint : .leading, - endPoint : .trailing, + colour : ColourStyle(stops: stops, + startPoint: .leading, + endPoint: .trailing), strokeStyle: nil, prioity : 1, chartType : .bar)) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 7ad12f77..1738e96a 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -217,7 +217,7 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { } // MARK: - Touch -extension GroupedBarChartData: TouchProtocol { +extension GroupedBarChartData { public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { @@ -285,34 +285,34 @@ extension GroupedBarChartData: LegendProtocol { for group in self.groups { - if group.colourType == .colour, - let colour = group.colour + if group.fillColour.colourType == .colour, + let colour = group.fillColour.colour { self.legends.append(LegendData(id : group.id, legend : group.title, - colour : colour, + colour : ColourStyle(colour: colour), strokeStyle: nil, prioity : 1, chartType : .bar)) - } else if group.colourType == .gradientColour, - let colours = group.colours + } else if group.fillColour.colourType == .gradientColour, + let colours = group.fillColour.colours { self.legends.append(LegendData(id : group.id, legend : group.title, - colours : colours, - startPoint : .leading, - endPoint : .trailing, + colour : ColourStyle(colours: colours, + startPoint: .leading, + endPoint: .trailing), strokeStyle: nil, prioity : 1, chartType : .bar)) - } else if group.colourType == .gradientStops, - let stops = group.stops + } else if group.fillColour.colourType == .gradientStops, + let stops = group.fillColour.stops { self.legends.append(LegendData(id : group.id, legend : group.title, - stops : stops, - startPoint : .leading, - endPoint : .trailing, + colour : ColourStyle(stops: stops, + startPoint: .leading, + endPoint: .trailing), strokeStyle: nil, prioity : 1, chartType : .bar)) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index bf418812..f7f17926 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -203,7 +203,7 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { } // MARK: - Touch -extension StackedBarChartData: TouchProtocol { +extension StackedBarChartData { public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { @@ -304,34 +304,34 @@ extension StackedBarChartData: LegendProtocol { public func setupLegends() { for group in self.groups { - if group.colourType == .colour, - let colour = group.colour + if group.fillColour.colourType == .colour, + let colour = group.fillColour.colour { self.legends.append(LegendData(id : group.id, legend : group.title, - colour : colour, + colour : ColourStyle(colour: colour), strokeStyle: nil, prioity : 1, chartType : .bar)) - } else if group.colourType == .gradientColour, - let colours = group.colours + } else if group.fillColour.colourType == .gradientColour, + let colours = group.fillColour.colours { self.legends.append(LegendData(id : group.id, legend : group.title, - colours : colours, - startPoint : .leading, - endPoint : .trailing, + colour : ColourStyle(colours: colours, + startPoint: .leading, + endPoint: .trailing), strokeStyle: nil, prioity : 1, chartType : .bar)) - } else if group.colourType == .gradientStops, - let stops = group.stops + } else if group.fillColour.colourType == .gradientStops, + let stops = group.fillColour.stops { self.legends.append(LegendData(id : group.id, legend : group.title, - stops : stops, - startPoint : .leading, - endPoint : .trailing, + colour : ColourStyle(stops: stops, + startPoint: .leading, + endPoint: .trailing), strokeStyle: nil, prioity : 1, chartType : .bar)) diff --git a/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift index 31bb721e..c657dfad 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift @@ -28,99 +28,26 @@ public struct BarChartDataPoint: CTStandardBarDataPoint { public var xAxisLabel : String? public var pointDescription : String? public var date : Date? - - public var colourType : ColourType - public var colour : Color? - public var colours : [Color]? - public var stops : [GradientStop]? - public var startPoint : UnitPoint? - public var endPoint : UnitPoint? + public var fillColour : ColourStyle // 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 + /// - 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. + /// - fillColour: Colour styling for the fill. 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: - 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: - 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 + fillColour : ColourStyle = ColourStyle(colour: .red) ) { 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 + self.fillColour = fillColour } } diff --git a/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift index 8d7f5932..c0e0b4fd 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift @@ -33,7 +33,7 @@ public struct MultiBarChartDataPoint: CTMultiBarDataPoint { xAxisLabel : String? = nil, pointLabel : String? = nil, date : Date? = nil, - group: GroupingData + group : GroupingData ) { self.value = value self.xAxisLabel = xAxisLabel diff --git a/Sources/SwiftUICharts/BarChart/Models/GroupingData.swift b/Sources/SwiftUICharts/BarChart/Models/GroupingData.swift index 5c3e475d..f56fbba9 100644 --- a/Sources/SwiftUICharts/BarChart/Models/GroupingData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/GroupingData.swift @@ -12,76 +12,23 @@ import SwiftUI # Example ``` - GroupingData(title: "One", colour: .blue) + GroupingData(title: "One", fillColour: ColourStyle(colour: .blue)) ``` */ -public struct GroupingData: CTColourStyle, Hashable, Identifiable { +public struct GroupingData: Hashable, Identifiable { public let id : UUID = UUID() public var title : String - 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 - /// Group with single colour - /// - Parameters: - /// - title: Title for legends - /// - colour: Colour drawing the bars and legends - public init(title : String, - colour : Color - ) { - self.title = title - self.colourType = .colour - self.colour = colour - self.colours = nil - self.stops = nil - self.startPoint = nil - self.endPoint = nil - } + public var fillColour: ColourStyle - // MARK: - Gradient colour - /// Group with gradient colours. - /// - Parameters: - /// - title: Title for legends - /// - colours: Colours drawing the bars and legends - /// - startPoint: Start point for Gradient. - /// - endPoint: End point for Gradient. - public init(title : String, - colours : [Color], - startPoint : UnitPoint, - endPoint : UnitPoint - ) { - self.title = title - self.colourType = .gradientColour - self.colour = nil - self.colours = colours - self.stops = nil - self.startPoint = startPoint - self.endPoint = endPoint - } - - // MARK: - Gradient with stops - /// Group with gradient colours and stops control. + /// Group with single colour /// - Parameters: /// - title: Title for legends - /// - stops: Colours drawing the bars and legends - /// - startPoint: Start point for Gradient. - /// - endPoint: End point for Gradient. - public init(title : String, - stops : [GradientStop], - startPoint : UnitPoint, - endPoint : UnitPoint + /// - fillColour: Colour styling for the bars. + public init(title : String, + fillColour: ColourStyle ) { self.title = title - self.colourType = .gradientStops - self.colour = nil - self.colours = nil - self.stops = stops - self.startPoint = startPoint - self.endPoint = endPoint + self.fillColour = fillColour } } diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index 878435eb..39c53324 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -12,7 +12,8 @@ import SwiftUI A protocol to extend functionality of `CTLineBarChartDataProtocol` specifically for Bar Charts. */ public protocol CTBarChartDataProtocol: CTLineBarChartDataProtocol { - + + associatedtype BarStyle : CTBarStyle /** Overall styling for the bars */ @@ -40,7 +41,16 @@ public protocol CTMultiBarChartDataProtocol: CTBarChartDataProtocol { */ public protocol CTBarChartStyle: CTLineBarChartStyle {} - +public protocol CTBarStyle: 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 fillColour : ColourStyle { get set } +} @@ -74,17 +84,20 @@ public protocol CTMultiBarChartDataSet: CTSingleDataSetProtocol {} // MARK: - DataPoints /** - A protocol to extend functionality of `CTLineBarDataPoint` specifically for standard Bar Charts. + A protocol to extend functionality of `CTLineBarDataPointProtocol` specifically for standard Bar Charts. */ -public protocol CTBarDataPoint: CTLineBarDataPoint {} +public protocol CTBarDataPoint: CTLineBarDataPointProtocol {} /** - A protocol to extend functionality of `CTLineBarDataPoint` specifically for standard Bar Charts. + A protocol to extend functionality of `CTLineBarDataPointProtocol` specifically for standard Bar Charts. */ -public protocol CTStandardBarDataPoint: CTBarDataPoint, CTColourStyle {} +public protocol CTStandardBarDataPoint: CTBarDataPoint { + /// Drawing style of the range fill. + var fillColour : ColourStyle { get set } +} /** - A protocol to extend functionality of `CTLineBarDataPoint` specifically for multi part Bar Charts. + A protocol to extend functionality of `CTLineBarDataPointProtocol` specifically for multi part Bar Charts. i.e: Grouped or Stacked */ public protocol CTMultiBarDataPoint: CTBarDataPoint { diff --git a/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift index 07076f51..932de398 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift @@ -15,24 +15,16 @@ import SwiftUI BarStyle(barWidth : 0.5, cornerRadius : CornerRadius(top: 15), colourFrom : .barStyle, - colour : .blue) + fillColour : ColourStyle(colour: .blue)) ``` */ -public struct BarStyle: CTColourStyle, Hashable { +public struct BarStyle: CTBarStyle { - /// 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 - - public var colourType : ColourType - public var colour : Color? - public var colours : [Color]? - public var stops : [GradientStop]? - public var startPoint : UnitPoint? - public var endPoint : UnitPoint? + public var barWidth : CGFloat + public var cornerRadius: CornerRadius + public var colourFrom : ColourFrom + + public var fillColour : ColourStyle // MARK: - Single colour /// Bar Chart with single colour @@ -40,74 +32,15 @@ public struct BarStyle: CTColourStyle, Hashable { /// - 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 + /// - fillColour: 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 - } - - // MARK: - Gradient 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 - } - - // MARK: - Gradient with stops - /// 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 + fillColour : ColourStyle = ColourStyle(colour: .red) ) { 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 + self.fillColour = fillColour } } diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index e935c198..8583d1e2 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -59,26 +59,26 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD HStack(spacing: 0) { ForEach(dataSet.dataPoints) { dataPoint in - if dataPoint.group.colourType == .colour, - let colour = dataPoint.group.colour + if dataPoint.group.fillColour.colourType == .colour, + let colour = dataPoint.group.fillColour.colour { ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) - .accessibilityLabel( Text("\(chartData.metadata.title)")) + .accessibilityLabel(Text("\(chartData.metadata.title)")) - } else if dataPoint.group.colourType == .gradientColour, - let colours = dataPoint.group.colours, - let startPoint = dataPoint.group.startPoint, - let endPoint = dataPoint.group.endPoint + } else if dataPoint.group.fillColour.colourType == .gradientColour, + let colours = dataPoint.group.fillColour.colours, + let startPoint = dataPoint.group.fillColour.startPoint, + let endPoint = dataPoint.group.fillColour.endPoint { GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) .accessibilityLabel( Text("\(chartData.metadata.title)")) - } else if dataPoint.group.colourType == .gradientStops, - let stops = dataPoint.group.stops, - let startPoint = dataPoint.group.startPoint, - let endPoint = dataPoint.group.endPoint + } else if dataPoint.group.fillColour.colourType == .gradientStops, + let stops = dataPoint.group.fillColour.stops, + let startPoint = dataPoint.group.fillColour.startPoint, + let endPoint = dataPoint.group.fillColour.endPoint { let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift index a6b6f608..509cfa18 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift @@ -24,24 +24,24 @@ internal struct BarChartDataSetSubView: View { } internal var body: some View { - if chartData.barStyle.colourType == .colour, - let colour = chartData.barStyle.colour + if chartData.barStyle.fillColour.colourType == .colour, + let colour = chartData.barStyle.fillColour.colour { ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) - } else if chartData.barStyle.colourType == .gradientColour, - let colours = chartData.barStyle.colours, - let startPoint = chartData.barStyle.startPoint, - let endPoint = chartData.barStyle.endPoint + } else if chartData.barStyle.fillColour.colourType == .gradientColour, + let colours = chartData.barStyle.fillColour.colours, + let startPoint = chartData.barStyle.fillColour.startPoint, + let endPoint = chartData.barStyle.fillColour.endPoint { GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) - } else if chartData.barStyle.colourType == .gradientStops, - let stops = chartData.barStyle.stops, - let startPoint = chartData.barStyle.startPoint, - let endPoint = chartData.barStyle.endPoint + } else if chartData.barStyle.fillColour.colourType == .gradientStops, + let stops = chartData.barStyle.fillColour.stops, + let startPoint = chartData.barStyle.fillColour.startPoint, + let endPoint = chartData.barStyle.fillColour.endPoint { let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) @@ -70,24 +70,24 @@ internal struct BarChartDataPointSubView: View { internal var body: some View { - if dataPoint.colourType == .colour, - let colour = dataPoint.colour + if dataPoint.fillColour.colourType == .colour, + let colour = dataPoint.fillColour.colour { ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) - } else if dataPoint.colourType == .gradientColour, - let colours = dataPoint.colours, - let startPoint = dataPoint.startPoint, - let endPoint = dataPoint.endPoint + } else if dataPoint.fillColour.colourType == .gradientColour, + let colours = dataPoint.fillColour.colours, + let startPoint = dataPoint.fillColour.startPoint, + let endPoint = dataPoint.fillColour.endPoint { GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) - } else if dataPoint.colourType == .gradientStops, - let stops = dataPoint.stops, - let startPoint = dataPoint.startPoint, - let endPoint = dataPoint.endPoint + } else if dataPoint.fillColour.colourType == .gradientStops, + let stops = dataPoint.fillColour.stops, + let startPoint = dataPoint.fillColour.startPoint, + let endPoint = dataPoint.fillColour.endPoint { let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift index 9640b091..9ab870f3 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift @@ -299,8 +299,8 @@ internal struct StackElementSubView: View { VStack(spacing: 0) { ForEach(dataSet.dataPoints.reversed()) { dataPoint in - if dataPoint.group.colourType == .colour, - let colour = dataPoint.group.colour + if dataPoint.group.fillColour.colourType == .colour, + let colour = dataPoint.group.fillColour.colour { ColourPartBar(colour, getHeight(height : geo.size.height, @@ -308,10 +308,10 @@ internal struct StackElementSubView: View { dataPoint : dataPoint)) .accessibilityValue(Text("\(dataPoint.value, specifier: "%.f"), \(dataPoint.pointDescription ?? "")")) - } else if dataPoint.group.colourType == .gradientColour, - let colours = dataPoint.group.colours, - let startPoint = dataPoint.group.startPoint, - let endPoint = dataPoint.group.endPoint + } else if dataPoint.group.fillColour.colourType == .gradientColour, + let colours = dataPoint.group.fillColour.colours, + let startPoint = dataPoint.group.fillColour.startPoint, + let endPoint = dataPoint.group.fillColour.endPoint { GradientColoursPartBar(colours, startPoint, endPoint, getHeight(height: geo.size.height, @@ -319,10 +319,10 @@ internal struct StackElementSubView: View { dataPoint : dataPoint)) .accessibilityValue(Text("\(dataPoint.value, specifier: "%.f") \(dataPoint.pointDescription ?? "")")) - } else if dataPoint.group.colourType == .gradientStops, - let stops = dataPoint.group.stops, - let startPoint = dataPoint.group.startPoint, - let endPoint = dataPoint.group.endPoint + } else if dataPoint.group.fillColour.colourType == .gradientStops, + let stops = dataPoint.group.fillColour.stops, + let startPoint = dataPoint.group.fillColour.startPoint, + let endPoint = dataPoint.group.fillColour.endPoint { let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) diff --git a/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift b/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift index c676a77c..5ed13d05 100644 --- a/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift @@ -10,7 +10,7 @@ import SwiftUI // MARK: - Paths extension Path { /// Draws straight lines between data points. - static func straightLine(rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, isFilled: Bool) -> Path { + static func straightLine(rect: CGRect, dataPoints: [DP], minValue: Double, range: Double, isFilled: Bool) -> Path { let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1) let y : CGFloat = rect.height / CGFloat(range) var path = Path() @@ -31,7 +31,7 @@ extension Path { } /// Draws cubic BĆ©zier curved lines between data points. - static func curvedLine(rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, isFilled: Bool) -> Path { + static func curvedLine(rect: CGRect, dataPoints: [DP], minValue: Double, range: Double, isFilled: Bool) -> Path { let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1) let y : CGFloat = rect.height / CGFloat(range) var path = Path() @@ -62,4 +62,78 @@ extension Path { } return path } + + + /// Draws straight lines between data points. + static func straightLineBox(rect: CGRect, dataPoints: [DP], minValue: Double, range: Double) -> 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) + 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) + 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) -> 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) + 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) + 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 index 9ca5b91e..af9b1054 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -55,12 +55,13 @@ public final class LineChartData: CTLineChartDataProtocol { @Published public var chartStyle : LineChartStyle @Published public var legends : [LegendData] @Published public var viewData : ChartViewData - @Published public var isFilled : Bool = false @Published public var infoView : InfoViewData = InfoViewData() public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) + internal var isFilled : Bool = false + // MARK: Initializer /// Initialises a Single Line Chart. /// @@ -146,7 +147,12 @@ public final class LineChartData: CTLineChartDataProtocol { } public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { - self.markerSubView(dataSet: self.dataSets, touchLocation: touchLocation, chartSize: chartSize) + self.markerSubView(markerType: self.chartStyle.markerType, + dataSet: dataSets, + dataPoints: dataSets.dataPoints, + lineType: dataSets.style.lineType, + touchLocation: touchLocation, + chartSize: chartSize) } // MARK: Accessibility @@ -169,7 +175,24 @@ public final class LineChartData: CTLineChartDataProtocol { } // MARK: - Touch -extension LineChartData: TouchProtocol { +extension LineChartData { + + 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 { + 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] = [] let xSection : CGFloat = chartSize.width / CGFloat(dataSets.dataPoints.count - 1) @@ -186,36 +209,36 @@ extension LineChartData: LegendProtocol { public func setupLegends() { - if dataSets.style.colourType == .colour, - let colour = dataSets.style.colour + if dataSets.style.lineColour.colourType == .colour, + let colour = dataSets.style.lineColour.colour { self.legends.append(LegendData(id : dataSets.id, legend : dataSets.legendTitle, - colour : colour, + colour : ColourStyle(colour: colour), strokeStyle: dataSets.style.strokeStyle, prioity : 1, chartType : .line)) - } else if dataSets.style.colourType == .gradientColour, - let colours = dataSets.style.colours + } else if dataSets.style.lineColour.colourType == .gradientColour, + let colours = dataSets.style.lineColour.colours { self.legends.append(LegendData(id : dataSets.id, legend : dataSets.legendTitle, - colours : colours, - startPoint : .leading, - endPoint : .trailing, + colour : ColourStyle(colours: colours, + startPoint: .leading, + endPoint: .trailing), strokeStyle: dataSets.style.strokeStyle, prioity : 1, chartType : .line)) - } else if dataSets.style.colourType == .gradientStops, - let stops = dataSets.style.stops + } else if dataSets.style.lineColour.colourType == .gradientStops, + let stops = dataSets.style.lineColour.stops { self.legends.append(LegendData(id : dataSets.id, legend : dataSets.legendTitle, - stops : stops, - startPoint : .leading, - endPoint : .trailing, + colour : ColourStyle(stops: stops, + startPoint: .leading, + endPoint: .trailing), strokeStyle: dataSets.style.strokeStyle, prioity : 1, chartType : .line)) diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index 04c06ab5..16448d73 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -62,12 +62,13 @@ public final class MultiLineChartData: CTLineChartDataProtocol { @Published public var chartStyle : LineChartStyle @Published public var legends : [LegendData] @Published public var viewData : ChartViewData - @Published public var isFilled : Bool = false @Published public var infoView : InfoViewData = InfoViewData() public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) + internal var isFilled : Bool = false + // MARK: Initializers /// Initialises a Multi Line Chart. /// @@ -157,7 +158,12 @@ public final class MultiLineChartData: CTLineChartDataProtocol { public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { ZStack { ForEach(self.dataSets.dataSets, id: \.self) { dataSet in - self.markerSubView(dataSet: dataSet, touchLocation: touchLocation, chartSize: chartSize) + self.markerSubView(markerType: self.chartStyle.markerType, + dataSet: dataSet, + dataPoints: dataSet.dataPoints, + lineType: dataSet.style.lineType, + touchLocation: touchLocation, + chartSize: chartSize) } } } @@ -189,8 +195,24 @@ public final class MultiLineChartData: CTLineChartDataProtocol { // MARK: - Touch -extension MultiLineChartData: TouchProtocol { +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 { + 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 { @@ -209,36 +231,36 @@ extension MultiLineChartData: LegendProtocol { public func setupLegends() { for dataSet in dataSets.dataSets { - if dataSet.style.colourType == .colour, - let colour = dataSet.style.colour + if dataSet.style.lineColour.colourType == .colour, + let colour = dataSet.style.lineColour.colour { self.legends.append(LegendData(id : dataSet.id, legend : dataSet.legendTitle, - colour : colour, + colour : ColourStyle(colour: colour), strokeStyle: dataSet.style.strokeStyle, prioity : 1, chartType : .line)) - } else if dataSet.style.colourType == .gradientColour, - let colours = dataSet.style.colours + } else if dataSet.style.lineColour.colourType == .gradientColour, + let colours = dataSet.style.lineColour.colours { self.legends.append(LegendData(id : dataSet.id, legend : dataSet.legendTitle, - colours : colours, - startPoint : .leading, - endPoint : .trailing, + colour : ColourStyle(colours: colours, + startPoint: .leading, + endPoint: .trailing), strokeStyle: dataSet.style.strokeStyle, prioity : 1, chartType : .line)) - } else if dataSet.style.colourType == .gradientStops, - let stops = dataSet.style.stops + } else if dataSet.style.lineColour.colourType == .gradientStops, + let stops = dataSet.style.lineColour.stops { self.legends.append(LegendData(id : dataSet.id, legend : dataSet.legendTitle, - stops : stops, - startPoint : .leading, - endPoint : .trailing, + colour : ColourStyle(stops: stops, + startPoint: .leading, + endPoint: .trailing), strokeStyle: dataSet.style.strokeStyle, prioity : 1, chartType : .line)) diff --git a/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift b/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift similarity index 95% rename from Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift rename to Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift index 5a345005..a673352e 100644 --- a/Sources/SwiftUICharts/LineChart/Models/LineChartDataPoint.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift @@ -18,7 +18,7 @@ import SwiftUI date : Date()) ``` */ -public struct LineChartDataPoint: CTLineBarDataPoint { +public struct LineChartDataPoint: CTLineDataPointProtocol { public let id : UUID = UUID() diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index 0a3b2048..3ffd2bef 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -13,43 +13,23 @@ import SwiftUI */ public protocol CTLineChartDataProtocol: CTLineBarChartDataProtocol { - /// A type representing opaque View - associatedtype Marker : View /// A type representing opaque View associatedtype Points : View - + /// A type representing opaque View associatedtype Access : View - - /** - Whether it is a normal or filled line. - */ - var isFilled : Bool { get set} /** - Returns the position to place the indicator on the line - based on the users touch or pointer input. + Displays Shapes over the data points. - - Parameters: - - rect: Frame of the path. - - dataPoints: Data points used to draw the chart. - - touchLocation: Location of the touch or pointer input. - - lineType: Drawing style of the line. - - Returns: The position to place the indicator. + - Returns: Relevent view containing point markers based the chosen parameters. */ - func getIndicatorLocation(rect: CGRect, dataPoints: [LineChartDataPoint], touchLocation: CGPoint, lineType: LineType) -> CGPoint - - /// Displays a view contatining touch markers. - /// - Parameters: - /// - dataSet: The data set to search in. - /// - touchLocation: Current location of the touch. - /// - chartSize: The size of the chart view as the parent view. - /// - Returns: Relevent touch marker based the chosen parameters. - func markerSubView(dataSet: LineDataSet, touchLocation: CGPoint, chartSize: CGRect) -> Marker - - /// 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 } @@ -60,6 +40,29 @@ public protocol CTLineChartDataProtocol: CTLineBarChartDataProtocol { */ public protocol CTLineChartStyle : CTLineBarChartStyle {} +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 } +} + +/** + 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 @@ -69,7 +72,7 @@ public protocol CTLineChartStyle : CTLineBarChartStyle {} public protocol CTLineChartDataSet: CTSingleDataSetProtocol { /// A type representing colour styling - associatedtype Styling : CTColourStyle + associatedtype Styling : CTLineStyle /** Label to display in the legend. @@ -89,3 +92,24 @@ public protocol CTLineChartDataSet: CTSingleDataSetProtocol { */ var pointStyle : PointStyle { get set } } + + + + + +// 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 `CTChartDataPointProtocol` specifically for Ranged Line Charts. + */ +public protocol CTRangedLineDataPoint: CTLineDataPointProtocol { + /// 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 } +} diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index 5031c878..677e8c3c 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -9,10 +9,11 @@ import SwiftUI // MARK: - Position Indicator extension CTLineChartDataProtocol { - public func getIndicatorLocation(rect: CGRect, - dataPoints: [LineChartDataPoint], - touchLocation: CGPoint, - lineType: LineType + + public func getIndicatorLocation(rect: CGRect, + dataPoints: [DP], + touchLocation: CGPoint, + lineType: LineType ) -> CGPoint { let path = getPath(lineType : lineType, @@ -25,6 +26,8 @@ extension CTLineChartDataProtocol { return self.locationOnPath(getPercentageOfPath(path: path, touchLocation: touchLocation), path) } +} +extension CTLineChartDataProtocol { /** Returns the relevent path based on the line type. @@ -38,7 +41,7 @@ extension CTLineChartDataProtocol { - isFilled: Whether it is a normal or filled line. - Returns: The relevent path based on the line type */ - func getPath(lineType: LineType, rect: CGRect, dataPoints: [LineChartDataPoint], minValue: Double, range: Double, touchLocation: CGPoint, isFilled: Bool) -> Path { + func getPath(lineType: LineType, rect: CGRect, dataPoints: [DP], minValue: Double, range: Double, touchLocation: CGPoint, isFilled: Bool) -> Path { switch lineType { case .line: return Path.straightLine(rect : rect, @@ -237,201 +240,188 @@ extension CTLineChartDataProtocol { // MARK: - Markers extension CTLineChartDataProtocol { - - 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 { - return CGPoint(x: CGFloat(index) * xSection, - y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.height) - } - return nil - } -} -extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { - @ViewBuilder - public func markerSubView(dataSet : LineDataSet, - touchLocation : CGPoint, - chartSize : CGRect - ) -> some View { - - 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: dataSet.dataPoints, - touchLocation: touchLocation, - lineType: dataSet.style.lineType)) - - case .vertical(attachment: let attach): - - switch attach { - case .line(dot: let indicator): - - let position = self.getIndicatorLocation(rect: chartSize, - dataPoints: dataSet.dataPoints, - touchLocation: touchLocation, - lineType: dataSet.style.lineType) + public func markerSubView + (markerType : LineMarkerType, + dataSet : DS, + dataPoints : [DP], + lineType : LineType, + touchLocation : CGPoint, + chartSize : CGRect) -> some View { + Group { + switch markerType { + case .none: + EmptyView() + case .indicator(let style): - Vertical(position: position) - .stroke(Color.primary, lineWidth: 2) + 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)) - IndicatorSwitch(indicator: indicator, location: position) + case .vertical(attachment: let attach): - case .point: - if let position = self.getPointLocation(dataSet: dataSet, - touchLocation: touchLocation, - chartSize: chartSize) { + switch attach { + case .line(dot: let indicator): + + let position = self.getIndicatorLocation(rect: chartSize, + dataPoints: dataPoints, + touchLocation: touchLocation, + lineType: lineType) + 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: dataSet.dataPoints, - touchLocation: touchLocation, - lineType: dataSet.style.lineType) - MarkerFull(position: position) - .stroke(Color.primary, lineWidth: 2) + case .full(attachment: let attach): - IndicatorSwitch(indicator: indicator, location: position) - - case .point: - - if let position = self.getPointLocation(dataSet: dataSet, - touchLocation: touchLocation, - chartSize: chartSize) { + switch attach { + case .line(dot: let indicator): + + let position = self.getIndicatorLocation(rect: chartSize, + dataPoints: dataPoints, + touchLocation: touchLocation, + lineType: lineType) MarkerFull(position: position) .stroke(Color.primary, lineWidth: 2) + + IndicatorSwitch(indicator: indicator, location: position) + + case .point: EmptyView() + + 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: dataSet.dataPoints, - touchLocation: touchLocation, - lineType: dataSet.style.lineType) + case .bottomLeading(attachment: let attach): - MarkerBottomLeading(position: position) - .stroke(Color.primary, lineWidth: 2) - - IndicatorSwitch(indicator: indicator, location: position) - - case .point: - - if let position = self.getPointLocation(dataSet: dataSet, - touchLocation: touchLocation, - chartSize: chartSize) { + switch attach { + case .line(dot: let indicator): + + let position = self.getIndicatorLocation(rect: chartSize, + dataPoints: dataPoints, + touchLocation: touchLocation, + lineType: lineType) MarkerBottomLeading(position: position) .stroke(Color.primary, lineWidth: 2) + + IndicatorSwitch(indicator: indicator, location: position) + + case .point: EmptyView() + + 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: dataSet.dataPoints, - touchLocation: touchLocation, - lineType: dataSet.style.lineType) - - MarkerBottomTrailing(position: position) - .stroke(Color.primary, lineWidth: 2) - - IndicatorSwitch(indicator: indicator, location: position) - case .point: + case .bottomTrailing(attachment: let attach): - if let position = self.getPointLocation(dataSet: dataSet, - touchLocation: touchLocation, - chartSize: chartSize) { + switch attach { + case .line(dot: let indicator): + + let position = self.getIndicatorLocation(rect: chartSize, + dataPoints: dataPoints, + touchLocation: touchLocation, + lineType: lineType) MarkerBottomTrailing(position: position) .stroke(Color.primary, lineWidth: 2) + + IndicatorSwitch(indicator: indicator, location: position) + + case .point: EmptyView() + + 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: dataSet.dataPoints, - touchLocation: touchLocation, - lineType: dataSet.style.lineType) - - MarkerTopLeading(position: position) - .stroke(Color.primary, lineWidth: 2) - IndicatorSwitch(indicator: indicator, location: position) + case .topLeading(attachment: let attach): - case .point: - - if let position = self.getPointLocation(dataSet: dataSet, - touchLocation: touchLocation, - chartSize: chartSize) { + switch attach { + case .line(dot: let indicator): + + let position = self.getIndicatorLocation(rect: chartSize, + dataPoints: dataPoints, + touchLocation: touchLocation, + lineType: lineType) MarkerTopLeading(position: position) .stroke(Color.primary, lineWidth: 2) + + IndicatorSwitch(indicator: indicator, location: position) + + case .point: EmptyView() + + 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: dataSet.dataPoints, - touchLocation: touchLocation, - lineType: dataSet.style.lineType) - MarkerTopTrailing(position: position) - .stroke(Color.primary, lineWidth: 2) + case .topTrailing(attachment: let attach): - IndicatorSwitch(indicator: indicator, location: position) - - case .point: - - if let position = self.getPointLocation(dataSet: dataSet, - touchLocation: touchLocation, - chartSize: chartSize) { + switch attach { + case .line(dot: let indicator): + + let position = self.getIndicatorLocation(rect: chartSize, + dataPoints: dataPoints, + touchLocation: touchLocation, + lineType: lineType) 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. */ diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift index 64d66013..d7f1f7c9 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift @@ -11,31 +11,16 @@ import SwiftUI Model for controlling the aesthetic of the line chart. # Example - ``` LineStyle(colour : .red, lineType : .curvedLine, strokeStyle: Stroke(lineWidth: 2)) ``` - */ -public struct LineStyle: CTColourStyle, Hashable { +public struct LineStyle: CTLineStyle, 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? - - /// Drawing style of the line + public var lineColour : ColourStyle public var lineType : LineType - - /** - Styling for stroke - - Replica of Appleā€™s StrokeStyle that conforms to Hashable - */ public var strokeStyle : Stroke /** @@ -50,11 +35,11 @@ public struct LineStyle: CTColourStyle, Hashable { // MARK: - Single colour /// Single Colour /// - Parameters: - /// - colour: Single Colour + /// - 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(colour : Color = Color(.red), + public init(lineColour : ColourStyle = ColourStyle(colour: .red), lineType : LineType = .curvedLine, strokeStyle : Stroke = Stroke(lineWidth : 3, lineCap : .round, @@ -64,88 +49,9 @@ public struct LineStyle: CTColourStyle, Hashable { dashPhase : 0), 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.ignoreZero = ignoreZero - } - - // MARK: - Gradient colour - /// 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 : Stroke = Stroke(lineWidth: 3, - lineCap: .round, - lineJoin: .round, - miterLimit: 10, - dash: [CGFloat](), - dashPhase: 0), - ignoreZero : Bool = false - ) { - self.colourType = .gradientColour - self.lineType = lineType + self.lineColour = lineColour + self.lineType = lineType self.strokeStyle = strokeStyle - - self.colour = nil - self.stops = nil - self.colours = colours - self.startPoint = startPoint - self.endPoint = endPoint - - self.ignoreZero = ignoreZero - } - - // 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. - /// - 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 : Stroke = Stroke(lineWidth: 3, - lineCap: .round, - lineJoin: .round, - miterLimit: 10, - dash: [CGFloat](), - dashPhase: 0), - 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.ignoreZero = ignoreZero + self.ignoreZero = ignoreZero } } - - diff --git a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift index 70199838..c9071cf9 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift @@ -10,16 +10,16 @@ import SwiftUI /** Main line shape */ -internal struct LineShape: Shape { +internal struct LineShape: Shape where DP: CTLineDataPointProtocol { - private let dataPoints : [LineChartDataPoint] + private let dataPoints : [DP] private let lineType : LineType private let isFilled : Bool private let minValue : Double private let range : Double - internal init(dataPoints: [LineChartDataPoint], + internal init(dataPoints: [DP], lineType : LineType, isFilled : Bool, minValue : Double, @@ -41,3 +41,39 @@ internal struct LineShape: Shape { } } } + +/** + 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 + + internal init(dataPoints: [DP], + lineType : LineType, + minValue : Double, + range : Double + ) { + self.dataPoints = dataPoints + self.lineType = lineType + self.minValue = minValue + self.range = range + } + + internal func path(in rect: CGRect) -> Path { + + switch lineType { + case .curvedLine: + return Path.curvedLineBox(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range) + case .line: + return Path.straightLineBox(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range) + } + + } +} + diff --git a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift index 332cacd8..dcd746f1 100644 --- a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift @@ -66,47 +66,47 @@ public struct FilledLineChart: View where ChartData: LineChartData { chartData.getAccessibility() - if chartData.dataSets.style.colourType == .colour, - let colour = chartData.dataSets.style.colour + 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) + dataSet : chartData.dataSets, + minValue : minValue, + range : range, + colour : colour, + isFilled : true) - } else if chartData.dataSets.style.colourType == .gradientColour, - let colours = chartData.dataSets.style.colours, - let startPoint = chartData.dataSets.style.startPoint, - let endPoint = chartData.dataSets.style.endPoint + } 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, + LineChartColoursSubView(chartData : chartData, + dataSet : chartData.dataSets, + minValue : minValue, + range : range, + colours : colours, startPoint: startPoint, - endPoint: endPoint, - isFilled: true) + endPoint : endPoint, + isFilled : true) - } else if chartData.dataSets.style.colourType == .gradientStops, - let stops = chartData.dataSets.style.stops, - let startPoint = chartData.dataSets.style.startPoint, - let endPoint = chartData.dataSets.style.endPoint + } 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) + LineChartStopsSubView(chartData : chartData, + dataSet : chartData.dataSets, + minValue : minValue, + range : range, + stops : stops, + startPoint : startPoint, + endPoint : endPoint, + isFilled : true) } } diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index 896008f2..2f04e4cd 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -47,15 +47,10 @@ public struct LineChart: View where ChartData: LineChartData { @ObservedObject var chartData: ChartData - private let minValue : Double - private let range : Double - /// Initialises a line chart view. /// - Parameter chartData: Must be LineChartData model. public init(chartData: ChartData) { self.chartData = chartData - self.minValue = chartData.minValue - self.range = chartData.range } public var body: some View { @@ -66,43 +61,43 @@ public struct LineChart: View where ChartData: LineChartData { chartData.getAccessibility() - if chartData.dataSets.style.colourType == .colour, - let colour = chartData.dataSets.style.colour + if chartData.dataSets.style.lineColour.colourType == .colour, + let colour = chartData.dataSets.style.lineColour.colour { LineChartColourSubView(chartData: chartData, dataSet : chartData.dataSets, - minValue : minValue, - range : range, + minValue : chartData.minValue, + range : chartData.range, colour : colour, isFilled : false) - } else if chartData.dataSets.style.colourType == .gradientColour, - let colours = chartData.dataSets.style.colours, - let startPoint = chartData.dataSets.style.startPoint, - let endPoint = chartData.dataSets.style.endPoint + } 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, + minValue : chartData.minValue, + range : chartData.range, colours : colours, startPoint : startPoint, endPoint : endPoint, isFilled : false) - } else if chartData.dataSets.style.colourType == .gradientStops, - let stops = chartData.dataSets.style.stops, - let startPoint = chartData.dataSets.style.startPoint, - let endPoint = chartData.dataSets.style.endPoint + } 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, + minValue : chartData.minValue, + range : chartData.range, stops : stops, startPoint: startPoint, endPoint : endPoint, diff --git a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift index 97c29872..4679d62c 100644 --- a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift @@ -67,47 +67,47 @@ public struct MultiLineChart: View where ChartData: MultiLineChartDat ForEach(chartData.dataSets.dataSets, id: \.id) { dataSet in - if dataSet.style.colourType == .colour, - let colour = dataSet.style.colour + 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) + dataSet : dataSet, + minValue : minValue, + range : range, + colour : colour, + isFilled : false) - } else if dataSet.style.colourType == .gradientColour, - let colours = dataSet.style.colours, - let startPoint = dataSet.style.startPoint, - let endPoint = dataSet.style.endPoint + } 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, + LineChartColoursSubView(chartData : chartData, + dataSet : dataSet, + minValue : minValue, + range : range, + colours : colours, startPoint: startPoint, - endPoint: endPoint, - isFilled: false) + endPoint : endPoint, + isFilled : false) - } else if dataSet.style.colourType == .gradientStops, - let stops = dataSet.style.stops, - let startPoint = dataSet.style.startPoint, - let endPoint = dataSet.style.endPoint + } 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, + LineChartStopsSubView(chartData : chartData, + dataSet : dataSet, + minValue : minValue, + range : range, + stops : stops, startPoint: startPoint, - endPoint: endPoint, - isFilled: false) + endPoint : endPoint, + isFilled : false) } } diff --git a/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift new file mode 100644 index 00000000..0bd7a051 --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift @@ -0,0 +1,134 @@ +// +// RangedLineChart.swift +// +// +// Created by Will Dale on 01/03/2021. +// + +import SwiftUI + +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() + + 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) + .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) + .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) + .fill(LinearGradient(gradient: Gradient(stops: stops), + startPoint: startPoint, + endPoint: endPoint)) + + } + + 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) } + } +} + + +/* + + ZStack { + RangedLineFillShape(dataPoints: chartData.dataSets.dataPoints, + lineType: chartData.dataSets.style.lineType, + minValue: chartData.minValue, + range: chartData.range) + .fill(Color.red.opacity(0.25)) + LineShape(dataPoints: chartData.dataSets.dataPoints, + lineType: chartData.dataSets.style.lineType, + isFilled: false, + minValue: chartData.minValue, + range: chartData.range) + .stroke(Color.blue, lineWidth: 3) + } + */ diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift index a5785775..80fd4f11 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift @@ -35,17 +35,19 @@ struct AccessibilityRectangle: Shape { Single colour */ -internal struct LineChartColourSubView: View where CD: CTLineChartDataProtocol { +internal struct LineChartColourSubView: View where CD: CTLineChartDataProtocol, + DS: CTLineChartDataSet, + DS.DataPoint: CTLineDataPointProtocol { private let chartData : CD - private let dataSet : LineDataSet + 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 : LineDataSet, + dataSet : DS, minValue : Double, range : Double, colour : Color, @@ -94,10 +96,12 @@ internal struct LineChartColourSubView: View where CD: CTLineChartDataProtoc Gradient colour */ -internal struct LineChartColoursSubView: View where CD: CTLineChartDataProtocol { +internal struct LineChartColoursSubView: View where CD: CTLineChartDataProtocol, + DS: CTLineChartDataSet, + DS.DataPoint: CTLineDataPointProtocol { private let chartData : CD - private let dataSet : LineDataSet + private let dataSet : DS private let minValue : Double private let range : Double @@ -108,7 +112,7 @@ internal struct LineChartColoursSubView: View where CD: CTLineChartDataProto private let isFilled : Bool internal init(chartData : CD, - dataSet : LineDataSet, + dataSet : DS, minValue : Double, range : Double, colours : [Color], @@ -174,10 +178,12 @@ internal struct LineChartColoursSubView: View where CD: CTLineChartDataProto Gradient with stops */ -internal struct LineChartStopsSubView: View where CD: CTLineChartDataProtocol { +internal struct LineChartStopsSubView: View where CD: CTLineChartDataProtocol, + DS: CTLineChartDataSet, + DS.DataPoint: CTLineDataPointProtocol { private let chartData : CD - private let dataSet : LineDataSet + private let dataSet : DS private let minValue : Double private let range : Double @@ -188,7 +194,7 @@ internal struct LineChartStopsSubView: View where CD: CTLineChartDataProtoco private let isFilled : Bool internal init(chartData : CD, - dataSet : LineDataSet, + dataSet : DS, minValue : Double, range : Double, stops : [Gradient.Stop], diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift index b0f75f79..2416a0f5 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift @@ -10,15 +10,15 @@ import SwiftUI /** Sub view gets the point markers drawn, sets the styling and sets up the animations. */ -internal struct PointsSubView: View { +internal struct PointsSubView: View where DS: CTLineChartDataSet { - private let dataSets: LineDataSet + private let dataSets : DS private let minValue : Double private let range : Double private let animation: Animation private let isFilled : Bool - internal init(dataSets : LineDataSet, + internal init(dataSets : DS, minValue : Double, range : Double, animation : Animation, diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift index 7d7895b0..7e57c83b 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift @@ -75,7 +75,7 @@ public final class DoughnutChartData: CTDoughnutChartDataProtocol { } // MARK: - Touch -extension DoughnutChartData: TouchProtocol { +extension DoughnutChartData { public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [PieChartDataPoint] = [] let touchDegree = degree(from: touchLocation, in: chartSize) diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift index d877acf0..31354d31 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift @@ -128,7 +128,7 @@ public final class MultiLayerPieChartData: CTMultiPieChartDataProtocol { } // MARK: - Touch -extension MultiLayerPieChartData: TouchProtocol { +extension MultiLayerPieChartData { public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { let points : [MultiPieDataPoint] = [] self.infoView.touchOverlayInfo = points diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift index 97022298..69bde6f4 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift @@ -76,7 +76,7 @@ public final class PieChartData: CTPieChartDataProtocol { } // MARK: - Touch -extension PieChartData: TouchProtocol { +extension PieChartData { public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [PieChartDataPoint] = [] let touchDegree = degree(from: touchLocation, in: chartSize) @@ -99,7 +99,7 @@ extension PieChartData: LegendProtocol { if let legend = data.pointDescription { self.legends.append(LegendData(id : data.id, legend : legend, - colour : data.colour, + colour : ColourStyle(colour: data.colour), strokeStyle: nil, prioity : 1, chartType : .pie)) diff --git a/Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift b/Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift index 2105e423..ab40d605 100644 --- a/Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift +++ b/Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift @@ -25,7 +25,7 @@ public struct MultiPieDataPoint: CTMultiPieChartDataPoint { // CTPieDataPoint public var startAngle : Double = 0 public var amount : Double = 0 - // CTChartDataPoint + // CTChartDataPointProtocol public var value : Double public var pointDescription : String? public var date : Date? diff --git a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift index b2cb860d..85c0cf50 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift @@ -44,9 +44,9 @@ public protocol CTMultiPieDataSet: CTDataSetProtocol {} // MARK: - DataPoints /** - A protocol to extend functionality of `CTChartDataPoint` specifically for Pie and Doughnut Charts. + A protocol to extend functionality of `CTChartDataPointProtocol` specifically for Pie and Doughnut Charts. */ -public protocol CTPieDataPoint: CTChartDataPoint { +public protocol CTPieDataPoint: CTChartDataPointProtocol { /** Where the data point should start drawing from @@ -62,7 +62,7 @@ public protocol CTPieDataPoint: CTChartDataPoint { var amount : Double { get set } } -public protocol CTMultiPieChartDataPoint: CTChartDataPoint { +public protocol CTMultiPieChartDataPoint: CTChartDataPointProtocol { /** Second layer of data points. diff --git a/Sources/SwiftUICharts/Shared/Models/ColourStyle.swift b/Sources/SwiftUICharts/Shared/Models/ColourStyle.swift new file mode 100644 index 00000000..91273bb3 --- /dev/null +++ b/Sources/SwiftUICharts/Shared/Models/ColourStyle.swift @@ -0,0 +1,78 @@ +// +// ColourStyle.swift +// +// +// Created by Will Dale on 02/03/2021. +// + +import SwiftUI + +//MARK: - Line +/** + Model for controlling the colours of `Stroke`. + + # Example + ``` + ColourStyle(colour: .red) + ``` + */ +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 index 2cf1afd3..802e82b3 100644 --- a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift @@ -10,7 +10,7 @@ import SwiftUI /** Data model to pass view information internally for the `InfoBox` and `HeaderBox`. */ -public struct InfoViewData { +public struct InfoViewData { /** Is there currently input (touch or click) on the chart. diff --git a/Sources/SwiftUICharts/Shared/Models/LegendData.swift b/Sources/SwiftUICharts/Shared/Models/LegendData.swift index 1a3d2d05..e41a5b97 100644 --- a/Sources/SwiftUICharts/Shared/Models/LegendData.swift +++ b/Sources/SwiftUICharts/Shared/Models/LegendData.swift @@ -10,10 +10,8 @@ import SwiftUI /** Data model to hold data for Legends */ - public struct LegendData: CTColourStyle, Hashable, Identifiable { - - // MARK: - Parameters - + public struct LegendData: Hashable, Identifiable { + public var id : UUID /// The type of chart being used. public var chartType : ChartType @@ -25,24 +23,18 @@ import SwiftUI /// Used to make sure the charts data legend is first public let prioity : Int - public var colourType : ColourType - public var colour : Color? - public var colours : [Color]? - public var stops : [GradientStop]? - public var startPoint : UnitPoint? - public var endPoint : UnitPoint? + public var colour : ColourStyle - // MARK: - Single Color - /// Legend with single colour + /// Legend. /// - Parameters: - /// - legend: Text to be displayed - /// - colour: Single Colour - /// - strokeStyle: Stroke Style - /// - prioity: Used to make sure the charts data legend is first + /// - 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 : Color, + colour : ColourStyle, strokeStyle: Stroke?, prioity : Int, chartType : ChartType @@ -50,77 +42,9 @@ import SwiftUI self.id = id 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 - self.colourType = .colour - } - - // MARK: - Gradient Color - /// Legend with a gradient colour - /// - 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 - /// - chartType: Type of chart being used. - public init(id : UUID, - legend : String, - colours : [Color], - startPoint : UnitPoint, - endPoint : UnitPoint, - 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 - self.colourType = .gradientColour - } - - // MARK: - Gradient Stops Color - /// 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 - /// - chartType: Type of chart being used. - public init(id : UUID, - legend : String, - stops : [GradientStop], - startPoint : UnitPoint, - endPoint : UnitPoint, - strokeStyle: Stroke?, - prioity : Int, - chartType : ChartType - ) { - self.id = id - self.legend = legend - self.colour = nil - self.colours = nil - self.stops = stops - self.startPoint = startPoint - self.endPoint = endPoint - self.strokeStyle = strokeStyle - self.prioity = prioity - self.chartType = chartType - self.colourType = .gradientStops + } } diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 3be289bc..fbd2f3d9 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -20,7 +20,7 @@ public protocol CTChartData: ObservableObject, Identifiable { associatedtype Set : CTDataSetProtocol /// A type representing a data point. -- `CTChartDataPoint` - associatedtype DataPoint: CTChartDataPoint + associatedtype DataPoint: CTChartDataPointProtocol /// A type representing the chart style. -- `CTChartStyle` associatedtype CTStyle : CTChartStyle @@ -28,6 +28,9 @@ public protocol CTChartData: ObservableObject, Identifiable { /// A type representing opaque View associatedtype Touch : View + /// A type representing a data set. -- `CTDataSetProtocol` + associatedtype SetPoint : CTDataSetProtocol + var id: ID { get } /** @@ -102,16 +105,6 @@ public protocol CTChartData: ObservableObject, Identifiable { */ func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> Touch -} - - - -// MARK: - Touch Protocol -public protocol TouchProtocol { - - /// A type representing a data set. -- `CTDataSetProtocol` - associatedtype SetPoint : CTDataSetProtocol - /** Gets the nearest data points to the touch location. - Parameters: @@ -133,6 +126,9 @@ public protocol TouchProtocol { } + + + // MARK: - Legend Protocol /** Protocol for dealing with legend data internally. @@ -167,7 +163,7 @@ public protocol CTDataSetProtocol: Hashable, Identifiable { */ public protocol CTSingleDataSetProtocol: CTDataSetProtocol { /// A type representing a data point. -- `CTChartDataPoint` - associatedtype DataPoint : CTChartDataPoint + associatedtype DataPoint : CTChartDataPointProtocol /** Array of data points. @@ -193,7 +189,7 @@ public protocol CTMultiDataSetProtocol: CTDataSetProtocol { /** Protocol to set base configuration for data points. */ -public protocol CTChartDataPoint: Hashable, Identifiable { +public protocol CTChartDataPointProtocol: Hashable, Identifiable { var id : ID { get } diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift index bf3db640..5c01056e 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift @@ -23,7 +23,7 @@ extension CTChartData where Set: CTMultiDataSetProtocol { } } // MARK: Touch -extension CTChartData where Self: TouchProtocol { +extension CTChartData { public func setTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) { self.infoView.isTouchCurrent = true self.infoView.touchLocation = touchLocation diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index bae79bff..a0c26369 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -91,7 +91,7 @@ internal struct LegendView: View where T: CTChartData { } } 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 + return chartData.infoView.isTouchCurrent && legend.colour == datapoint.group.fillColour } else { return false } @@ -114,11 +114,11 @@ internal struct LegendView: View where T: CTChartData { } /// Returns a Line legend. - func line(_ legend: LegendData) -> some View { + private func line(_ legend: LegendData) -> some View { Group { if let stroke = legend.strokeStyle { let strokeStyle = stroke.strokeToStrokeStyle() - if let colour = legend.colour { + if let colour = legend.colour.colour { HStack { LegendLine(width: 40) .stroke(colour, style: strokeStyle) @@ -128,7 +128,7 @@ internal struct LegendView: View where T: CTChartData { .foregroundColor(textColor) } - } else if let colours = legend.colours { + } else if let colours = legend.colour.colours { HStack { LegendLine(width: 40) .stroke(LinearGradient(gradient: Gradient(colors: colours), @@ -140,7 +140,7 @@ internal struct LegendView: View where T: CTChartData { .font(.caption) .foregroundColor(textColor) } - } else if let stops = legend.stops { + } else if let stops = legend.colour.stops { let stops = GradientStop.convertToGradientStopsArray(stops: stops) HStack { LegendLine(width: 40) @@ -159,9 +159,9 @@ internal struct LegendView: View where T: CTChartData { } /// Returns a Bar legend. - func bar(_ legend: LegendData) -> some View { + private func bar(_ legend: LegendData) -> some View { Group { - if let colour = legend.colour + if let colour = legend.colour.colour { HStack { Rectangle() @@ -170,9 +170,9 @@ internal struct LegendView: View where T: CTChartData { Text(legend.legend) .font(.caption) } - } else if let colours = legend.colours, - let startPoint = legend.startPoint, - let endPoint = legend.endPoint + } else if let colours = legend.colour.colours, + let startPoint = legend.colour.startPoint, + let endPoint = legend.colour.endPoint { HStack { Rectangle() @@ -183,9 +183,9 @@ internal struct LegendView: View where T: CTChartData { Text(legend.legend) .font(.caption) } - } else if let stops = legend.stops, - let startPoint = legend.startPoint, - let endPoint = legend.endPoint + } else if let stops = legend.colour.stops, + let startPoint = legend.colour.startPoint, + let endPoint = legend.colour.endPoint { let stops = GradientStop.convertToGradientStopsArray(stops: stops) HStack { @@ -202,9 +202,9 @@ internal struct LegendView: View where T: CTChartData { } /// Returns a Pie legend. - func pie(_ legend: LegendData) -> some View { + private func pie(_ legend: LegendData) -> some View { Group { - if let colour = legend.colour { + if let colour = legend.colour.colour { HStack { Circle() .fill(colour) @@ -213,9 +213,9 @@ internal struct LegendView: View where T: CTChartData { .font(.caption) } - } else if let colours = legend.colours, - let startPoint = legend.startPoint, - let endPoint = legend.endPoint + } else if let colours = legend.colour.colours, + let startPoint = legend.colour.startPoint, + let endPoint = legend.colour.endPoint { HStack { Circle() @@ -227,9 +227,9 @@ internal struct LegendView: View where T: CTChartData { .font(.caption) } - } else if let stops = legend.stops, - let startPoint = legend.startPoint, - let endPoint = legend.endPoint + } else if let stops = legend.colour.stops, + let startPoint = legend.colour.startPoint, + let endPoint = legend.colour.endPoint { let stops = GradientStop.convertToGradientStopsArray(stops: stops) HStack { diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index 6163f938..e7fd6759 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -10,7 +10,7 @@ import SwiftUI /** View that displays information from the touch events. */ -internal struct TouchOverlayBox: View { +internal struct TouchOverlayBox: View { private var isTouchCurrent : Bool private var selectedPoints : [D] diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index c16292f2..349d9323 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -142,9 +142,9 @@ public protocol CTLineBarChartStyle: CTChartStyle { // MARK: - DataPoints /** - A protocol to extend functionality of `CTChartDataPoint` specifically for Line and Bar Charts. + A protocol to extend functionality of `CTChartDataPointProtocol` specifically for Line and Bar Charts. */ -public protocol CTLineBarDataPoint: CTChartDataPoint { +public protocol CTLineBarDataPointProtocol: CTChartDataPointProtocol { /** Data points label for the X axis. diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index f5774e6c..af8df0b4 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -75,7 +75,7 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { if !chartData.legends.contains(where: { $0.legend == markerName }) { // init twice chartData.legends.append(LegendData(id : uuid, legend : markerName, - colour : lineColour, + colour : ColourStyle(colour: lineColour), strokeStyle : strokeStyle.toStroke(), prioity : 2, chartType : .line)) @@ -110,7 +110,7 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { labelBackground: labelBackground, lineColour : lineColour, chartSize : geo.frame(in: .local)) - .accessibilityLabel( Text("P O I Marker")) + .accessibilityLabel(Text("P O I Marker")) .accessibilityValue(Text("\(markerName), \(markerValue, specifier: specifier)")) case .center(specifier: let specifier): From ec3ddb3a9963e8697c790cd631c30b37c3fc64fa Mon Sep 17 00:00:00 2001 From: Will Dale Date: Tue, 2 Mar 2021 13:54:23 +0000 Subject: [PATCH 111/152] Add ranged line chart. --- .../ChartData/RangedLineChartData.swift | 192 ++++++++++++++++++ .../DataPoints/RangedLineChartDataPoint.swift | 56 +++++ .../Models/DataSet/RangedLineDataSet.swift | 62 ++++++ .../Models/Style/RangedLineStyle.swift | 61 ++++++ 4 files changed, 371 insertions(+) create mode 100644 Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift create mode 100644 Sources/SwiftUICharts/LineChart/Models/DataPoints/RangedLineChartDataPoint.swift create mode 100644 Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift create mode 100644 Sources/SwiftUICharts/LineChart/Models/Style/RangedLineStyle.swift diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift new file mode 100644 index 00000000..627052c5 --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift @@ -0,0 +1,192 @@ +// +// RangedLineChartData.swift +// +// +// Created by Will Dale on 01/03/2021. +// + +import SwiftUI + +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) + + internal 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 : 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() + } + // MARK: Data + public var range : Double { + + var _lowestValue : Double + var _highestValue : Double + + switch self.chartStyle.baseline { + case .minimumValue: + _lowestValue = dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0 + case .minimumWithMaximum(of: let value): + _lowestValue = min(dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0, value) + case .zero: + _lowestValue = 0 + } + + switch self.chartStyle.topLine { + case .maximumValue: + _highestValue = dataSets.dataPoints.max(by: { $0.upperValue < $1.upperValue })?.upperValue ?? 0 + case .maximum(of: let value): + _highestValue = max(dataSets.dataPoints.max(by: { $0.upperValue < $1.upperValue })?.upperValue ?? 0, value) + } + + return _highestValue - _lowestValue + } + + public var minValue : Double { + switch self.chartStyle.baseline { + case .minimumValue: + return dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0 + case .minimumWithMaximum(of: let value): + return min(dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0, value) + case .zero: + return 0 + } + } + + + // MARK: Labels + public func getXAxisLabels() -> some View { + Group { + switch self.chartStyle.xAxisLabelsFrom { + case .dataPoint: + + HStack(spacing: 0) { + ForEach(dataSets.dataPoints) { data in + if let label = data.xAxisLabel { + Text(label) + .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) + .lineLimit(1) + .minimumScaleFactor(0.5) + .accessibilityLabel( Text("X Axis Label")) + .accessibilityValue(Text("\(data.xAxisLabel ?? "")")) + } + 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 + Text(data) + .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) + .lineLimit(1) + .minimumScaleFactor(0.5) + .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 : self.isFilled) + } + + public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { + self.markerSubView(markerType: self.chartStyle.markerType, + 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 { + 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 { + points.append(dataSets.dataPoints[index]) + } + self.infoView.touchOverlayInfo = points + } + + // MARK: Accessibility + public func getAccessibility() -> some View { + EmptyView() + } + + + public typealias Set = RangedLineDataSet + public typealias DataPoint = RangedLineChartDataPoint +} diff --git a/Sources/SwiftUICharts/LineChart/Models/DataPoints/RangedLineChartDataPoint.swift b/Sources/SwiftUICharts/LineChart/Models/DataPoints/RangedLineChartDataPoint.swift new file mode 100644 index 00000000..6dfa0fc7 --- /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", + pointLabel: "Monday") + ``` + */ +public struct RangedLineChartDataPoint: CTRangedLineDataPoint { + + public let id : UUID = UUID() + + public var value : Double + public var xAxisLabel : String? + public var pointDescription : String? + public var date : Date? + + public var upperValue : Double + public var lowerValue : Double + + /// 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. + /// - pointLabel: 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, + pointLabel : String? = nil, + date : Date? = nil + ) { + self.value = value + self.upperValue = upperValue + self.lowerValue = lowerValue + self.xAxisLabel = xAxisLabel + self.pointDescription = pointLabel + self.date = date + } +} diff --git a/Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift new file mode 100644 index 00000000..8fda29fc --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift @@ -0,0 +1,62 @@ +// +// 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. + + # Example + ``` + RangedLineDataSet(dataPoints: [ + RangedLineChartDataPoint(value: 10, upperValue: 20, lowerValue: 0 , xAxisLabel: "M", pointLabel: "Monday"), + RangedLineChartDataPoint(value: 25, upperValue: 35, lowerValue: 15, xAxisLabel: "T", pointLabel: "Tuesday"), + RangedLineChartDataPoint(value: 13, upperValue: 23, lowerValue: 3 , xAxisLabel: "W", pointLabel: "Wednesday"), + RangedLineChartDataPoint(value: 24, upperValue: 34, lowerValue: 14, xAxisLabel: "T", pointLabel: "Thursday"), + RangedLineChartDataPoint(value: 36, upperValue: 46, lowerValue: 26, xAxisLabel: "F", pointLabel: "Friday"), + RangedLineChartDataPoint(value: 14, upperValue: 24, lowerValue: 4 , xAxisLabel: "S", pointLabel: "Saturday"), + RangedLineChartDataPoint(value: 20, upperValue: 30, lowerValue: 10, xAxisLabel: "S", pointLabel: "Sunday") + ], + legendTitle: "Steps", + pointStyle: PointStyle(), + style: RangedLineStyle(lineColour: ColourStyle(colour: .red), + fillColour: ColourStyle(colour: Color(.blue).opacity(0.25)), + lineType: .curvedLine)) + ``` + */ +public struct RangedLineDataSet: CTLineChartDataSet { + + public let id : UUID = UUID() + public var dataPoints : [RangedLineChartDataPoint] + public var legendTitle : String + public var pointStyle : PointStyle + public var style : RangedLineStyle + + + /// 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 : [RangedLineChartDataPoint], + legendTitle : String = "", + pointStyle : PointStyle = PointStyle(), + style : RangedLineStyle = RangedLineStyle() + ) { + self.dataPoints = dataPoints + self.legendTitle = legendTitle + self.pointStyle = pointStyle + self.style = style + } + + public typealias ID = UUID + public typealias Styling = RangedLineStyle + +} diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/RangedLineStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/RangedLineStyle.swift new file mode 100644 index 00000000..daa0d7c9 --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Models/Style/RangedLineStyle.swift @@ -0,0 +1,61 @@ +// +// RangedLineStyle.swift +// +// +// Created by Will Dale on 02/03/2021. +// + +import SwiftUI +/** + Model for controlling the aesthetic of the ranged line chart. + + # Example + ``` + RangedLineStyle(lineColour: ColourStyle(colour: .red), + fillColour: ColourStyle(colour: Color(.blue).opacity(0.25)), + lineType : .curvedLine)) + ``` + */ +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 + } +} From a53f49f0a462fca0e26803c1fb5b129bf2b44617 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 3 Mar 2021 10:20:48 +0000 Subject: [PATCH 112/152] Refactor legends into protocol extensions. --- .../Models/ChartData/BarChartData.swift | 86 ------------ .../ChartData/GroupedBarChartData.swift | 47 ------- .../ChartData/StackedBarChartData.swift | 45 ------- .../Models/Protocols/BarChartProtocols.swift | 124 +++++++++++++++++ .../Models/ChartData/LineChartData.swift | 44 +----- .../Models/ChartData/MultiLineChartData.swift | 47 ------- .../ChartData/RangedLineChartData.swift | 5 +- .../Models/DataSet/MultiLineDataSet.swift | 2 +- .../Models/DataSet/RangedLineDataSet.swift | 22 +-- .../Models/Protocols/LineChartProtocols.swift | 125 +++++++++++++++++- .../LineChartProtocolsExtensions.swift | 83 +++++++----- .../Models/ChartData/DoughnutChartData.swift | 9 -- .../ChartData/MultiLayerPieChartData.swift | 9 -- .../Models/ChartData/PieChartData.swift | 20 --- .../PieChart/Models/DataSets/PieDataSet.swift | 3 +- .../Models/Protocols/PieChartProtocols.swift | 24 +++- .../PieChart/Views/DoughnutChart.swift | 1 + .../PieChart/Views/MultiLayerPieChart.swift | 1 + .../PieChart/Views/PieChart.swift | 2 + .../Models/Protocols/SharedProtocols.swift | 25 ---- .../Protocols/SharedProtocolsExtensions.swift | 5 + .../LineAndBarProtocolsExtentions.swift | 4 +- .../BarCharts/BarChartTests.swift | 38 ++++-- .../BarCharts/GroupedBarChartTests.swift | 8 +- .../BarCharts/StackedBarChartTests.swift | 8 +- .../LineCharts/LineChartPathTests.swift | 20 +-- 26 files changed, 396 insertions(+), 411 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 2b0f0914..bc04f961 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -196,89 +196,3 @@ extension BarChartData { return nil } } - -// MARK: - Legends -extension BarChartData: LegendProtocol { - public func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } - - public func setupLegends() { - - switch self.barStyle.colourFrom { - case .barStyle: - if self.barStyle.fillColour.colourType == .colour, - let colour = self.barStyle.fillColour.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.fillColour.colourType == .gradientColour, - let colours = self.barStyle.fillColour.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.fillColour.colourType == .gradientStops, - let stops = self.barStyle.fillColour.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.fillColour.colourType == .colour, - let colour = data.fillColour.colour, - let legend = data.pointDescription - { - self.legends.append(LegendData(id : data.id, - legend : legend, - colour : ColourStyle(colour: colour), - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if data.fillColour.colourType == .gradientColour, - let colours = data.fillColour.colours, - let legend = data.pointDescription - { - 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.fillColour.colourType == .gradientStops, - let stops = data.fillColour.stops, - let legend = data.pointDescription - { - self.legends.append(LegendData(id : data.id, - legend : legend, - colour : ColourStyle(stops: stops, - startPoint: .leading, - endPoint: .trailing), - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } - } - } - } -} diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 1738e96a..8575bf08 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -277,50 +277,3 @@ extension GroupedBarChartData { return nil } } - -// MARK: - Legends -extension GroupedBarChartData: LegendProtocol { - - public func setupLegends() { - - for group in self.groups { - - if group.fillColour.colourType == .colour, - let colour = group.fillColour.colour - { - self.legends.append(LegendData(id : group.id, - legend : group.title, - colour : ColourStyle(colour: colour), - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if group.fillColour.colourType == .gradientColour, - let colours = group.fillColour.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.fillColour.colourType == .gradientStops, - let stops = group.fillColour.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)) - } - } - } - - public func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } -} diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index f7f17926..a9871870 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -298,48 +298,3 @@ extension StackedBarChartData { return nil } } - -extension StackedBarChartData: LegendProtocol { - // MARK: - Legends - public func setupLegends() { - for group in self.groups { - - if group.fillColour.colourType == .colour, - let colour = group.fillColour.colour - { - self.legends.append(LegendData(id : group.id, - legend : group.title, - colour : ColourStyle(colour: colour), - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if group.fillColour.colourType == .gradientColour, - let colours = group.fillColour.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.fillColour.colourType == .gradientStops, - let stops = group.fillColour.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)) - } - } - } - - public func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } -} diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index 39c53324..877be885 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -20,6 +20,90 @@ public protocol CTBarChartDataProtocol: CTLineBarChartDataProtocol { var barStyle : BarStyle { get set } } +extension CTBarChartDataProtocol where Self.Set.ID == UUID, + Self.Set.DataPoint.ID == UUID, + Self.Set: CTStandardBarChartDataSet, + Self.Set.DataPoint: CTStandardBarDataPoint { + internal func setupLegends() { + + switch self.barStyle.colourFrom { + case .barStyle: + if self.barStyle.fillColour.colourType == .colour, + let colour = self.barStyle.fillColour.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.fillColour.colourType == .gradientColour, + let colours = self.barStyle.fillColour.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.fillColour.colourType == .gradientStops, + let stops = self.barStyle.fillColour.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.fillColour.colourType == .colour, + let colour = data.fillColour.colour, + let legend = data.pointDescription + { + self.legends.append(LegendData(id : data.id, + legend : legend, + colour : ColourStyle(colour: colour), + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if data.fillColour.colourType == .gradientColour, + let colours = data.fillColour.colours, + let legend = data.pointDescription + { + 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.fillColour.colourType == .gradientStops, + let stops = data.fillColour.stops, + let legend = data.pointDescription + { + self.legends.append(LegendData(id : data.id, + legend : legend, + colour : ColourStyle(stops: stops, + startPoint: .leading, + endPoint: .trailing), + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } + } + } + } +} + /** A protocol to extend functionality of `CTBarChartDataProtocol` specifically for Multi Part Bar Charts. */ @@ -31,6 +115,46 @@ public protocol CTMultiBarChartDataProtocol: CTBarChartDataProtocol { var groups : [GroupingData] { get set } } +extension CTMultiBarChartDataProtocol { + internal func setupLegends() { + + for group in self.groups { + + if group.fillColour.colourType == .colour, + let colour = group.fillColour.colour + { + self.legends.append(LegendData(id : group.id, + legend : group.title, + colour : ColourStyle(colour: colour), + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if group.fillColour.colourType == .gradientColour, + let colours = group.fillColour.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.fillColour.colourType == .gradientStops, + let stops = group.fillColour.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/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index af9b1054..5398fc80 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -205,47 +205,7 @@ extension LineChartData { } // MARK: - Legends -extension LineChartData: LegendProtocol { +extension LineChartData { - public func setupLegends() { - - if dataSets.style.lineColour.colourType == .colour, - let colour = dataSets.style.lineColour.colour - { - self.legends.append(LegendData(id : dataSets.id, - legend : dataSets.legendTitle, - colour : ColourStyle(colour: colour), - strokeStyle: dataSets.style.strokeStyle, - prioity : 1, - chartType : .line)) - - } else if dataSets.style.lineColour.colourType == .gradientColour, - let colours = dataSets.style.lineColour.colours - { - self.legends.append(LegendData(id : dataSets.id, - legend : dataSets.legendTitle, - colour : ColourStyle(colours: colours, - startPoint: .leading, - endPoint: .trailing), - strokeStyle: dataSets.style.strokeStyle, - prioity : 1, - chartType : .line)) - - } else if dataSets.style.lineColour.colourType == .gradientStops, - let stops = dataSets.style.lineColour.stops - { - self.legends.append(LegendData(id : dataSets.id, - legend : dataSets.legendTitle, - colour : ColourStyle(stops: stops, - startPoint: .leading, - endPoint: .trailing), - strokeStyle: dataSets.style.strokeStyle, - prioity : 1, - chartType : .line)) - } - } - - public func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } + } diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index 16448d73..092c9d20 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -225,50 +225,3 @@ extension MultiLineChartData { self.infoView.touchOverlayInfo = points } } - -// MARK: - Legends -extension MultiLineChartData: LegendProtocol { - - public func setupLegends() { - for dataSet in dataSets.dataSets { - 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)) - } - } - } - - public func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } -} diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift index 627052c5..27021337 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift @@ -49,7 +49,8 @@ public final class RangedLineChartData: CTLineChartDataProtocol { self.viewData = ChartViewData() self.chartType = (chartType: .line, dataSetType: .single) -// self.setupLegends() + self.setupLegends() + self.setupRangeLegends() } // MARK: Data public var range : Double { @@ -73,7 +74,7 @@ public final class RangedLineChartData: CTLineChartDataProtocol { _highestValue = max(dataSets.dataPoints.max(by: { $0.upperValue < $1.upperValue })?.upperValue ?? 0, value) } - return _highestValue - _lowestValue + return (_highestValue - _lowestValue) + 0.001 } public var minValue : Double { diff --git a/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift index a2747826..9ad14b60 100644 --- a/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift @@ -42,7 +42,7 @@ MultiLineDataSet(dataSets: [ ]) ``` */ -public struct MultiLineDataSet: CTMultiDataSetProtocol { +public struct MultiLineDataSet: CTMultiLineChartDataSet { public let id : UUID = UUID() public var dataSets : [LineDataSet] diff --git a/Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift index 8fda29fc..da943e32 100644 --- a/Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift @@ -30,11 +30,12 @@ import SwiftUI lineType: .curvedLine)) ``` */ -public struct RangedLineDataSet: CTLineChartDataSet { +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 @@ -43,17 +44,20 @@ public struct RangedLineDataSet: CTLineChartDataSet { /// - 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 = "", - pointStyle : PointStyle = PointStyle(), - style : RangedLineStyle = RangedLineStyle() + public init(dataPoints : [RangedLineChartDataPoint], + legendTitle : String = "", + legendFillTitle : String = "", + pointStyle : PointStyle = PointStyle(), + style : RangedLineStyle = RangedLineStyle() ) { - self.dataPoints = dataPoints - self.legendTitle = legendTitle - self.pointStyle = pointStyle - self.style = style + self.dataPoints = dataPoints + self.legendTitle = legendTitle + self.legendFillTitle = legendFillTitle + self.pointStyle = pointStyle + self.style = style } public typealias ID = UUID diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index 3ffd2bef..e236f83c 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -33,6 +33,126 @@ public protocol CTLineChartDataProtocol: CTLineBarChartDataProtocol { func getAccessibility() -> Access } +extension CTLineChartDataProtocol where Self.Set.ID == UUID, + Self.Set: CTLineChartDataSet { + internal func setupLegends() { + + if dataSets.style.lineColour.colourType == .colour, + let colour = dataSets.style.lineColour.colour + { + self.legends.append(LegendData(id : dataSets.id, + legend : dataSets.legendTitle, + colour : ColourStyle(colour: colour), + strokeStyle: dataSets.style.strokeStyle, + prioity : 1, + chartType : .line)) + + } else if dataSets.style.lineColour.colourType == .gradientColour, + let colours = dataSets.style.lineColour.colours + { + self.legends.append(LegendData(id : dataSets.id, + legend : dataSets.legendTitle, + colour : ColourStyle(colours: colours, + startPoint: .leading, + endPoint: .trailing), + strokeStyle: dataSets.style.strokeStyle, + prioity : 1, + chartType : .line)) + + } else if dataSets.style.lineColour.colourType == .gradientStops, + let stops = dataSets.style.lineColour.stops + { + self.legends.append(LegendData(id : dataSets.id, + legend : dataSets.legendTitle, + colour : ColourStyle(stops: stops, + startPoint: .leading, + endPoint: .trailing), + strokeStyle: dataSets.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 : .line)) + + } 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 : .line)) + } + } +} +extension CTLineChartDataProtocol where Self.Set == MultiLineDataSet { + internal func setupLegends() { + for dataSet in dataSets.dataSets { + 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)) + } + } + } +} // MARK: - Style /** @@ -92,8 +212,11 @@ public protocol CTLineChartDataSet: CTSingleDataSetProtocol { */ var pointStyle : PointStyle { get set } } +public protocol CTRangedLineChartDataSet: CTLineChartDataSet { + var legendFillTitle : String { get set } +} - +public protocol CTMultiLineChartDataSet: CTMultiDataSetProtocol {} diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index 677e8c3c..61e2fe1a 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -10,21 +10,22 @@ import SwiftUI // MARK: - Position Indicator extension CTLineChartDataProtocol { - public func getIndicatorLocation(rect: CGRect, - dataPoints: [DP], - touchLocation: CGPoint, - lineType: LineType + public static func getIndicatorLocation(rect: CGRect, + dataPoints: [DP], + touchLocation: CGPoint, + lineType: LineType, + minValue: Double, + range: Double ) -> CGPoint { - let path = getPath(lineType : lineType, + let path = Self.getPath(lineType : lineType, rect : rect, dataPoints : dataPoints, - minValue : self.minValue, - range : self.range, + minValue : minValue, + range : range, touchLocation: touchLocation, isFilled : false) - - return self.locationOnPath(getPercentageOfPath(path: path, touchLocation: touchLocation), path) + return Self.locationOnPath(Self.getPercentageOfPath(path: path, touchLocation: touchLocation), path) } } extension CTLineChartDataProtocol { @@ -41,7 +42,7 @@ extension CTLineChartDataProtocol { - isFilled: Whether it is a normal or filled line. - Returns: The relevent path based on the line type */ - func getPath(lineType: LineType, rect: CGRect, dataPoints: [DP], minValue: Double, range: Double, touchLocation: CGPoint, isFilled: Bool) -> Path { + static func getPath(lineType: LineType, rect: CGRect, dataPoints: [DP], minValue: Double, range: Double, touchLocation: CGPoint, isFilled: Bool) -> Path { switch lineType { case .line: return Path.straightLine(rect : rect, @@ -66,7 +67,7 @@ extension CTLineChartDataProtocol { - touchLocation: Location of the touch or pointer input. - Returns: How far along the path the touch is. */ - func getPercentageOfPath(path: Path, touchLocation: CGPoint) -> CGFloat { + 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 @@ -82,7 +83,7 @@ extension CTLineChartDataProtocol { - Parameter path: Path to measure. - Returns: Total length of the path. */ - public func getTotalLength(of path: Path) -> CGFloat { + static func getTotalLength(of path: Path) -> CGFloat { var total : CGFloat = 0 var currentPoint: CGPoint = .zero path.forEach { (element) in @@ -99,7 +100,8 @@ extension CTLineChartDataProtocol { total += distance(from: currentPoint, to: next) currentPoint = next case .closeSubpath: - print("No reason for this to fire") + // No reason for this to fire + total += 0 } } return total @@ -113,7 +115,7 @@ extension CTLineChartDataProtocol { - path: Path to take measurement from. - Returns: Length of path to touch point. */ - func getLength(to touchLocation: CGPoint, on path: Path) -> CGFloat { + static func getLength(to touchLocation: CGPoint, on path: Path) -> CGFloat { var total : CGFloat = 0 var currentPoint: CGPoint = .zero var isComplete : Bool = false @@ -161,7 +163,8 @@ extension CTLineChartDataProtocol { currentPoint = nextPoint } case .closeSubpath: - print("No reason for this to fire") + // No reason for this to fire + total += 0 } } @@ -178,7 +181,7 @@ extension CTLineChartDataProtocol { - touchX: Location on the X axis of the touch or pointer input. - Returns: A point on the path */ - func relativePoint(from: CGPoint, to: CGPoint, touchX: CGFloat) -> CGPoint { + 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))) } @@ -192,7 +195,7 @@ extension CTLineChartDataProtocol { - touchX: Location on the X axis of the touch or pointer input. - Returns: Length from of a path element to touch location */ - func distanceToTouch(from: CGPoint, to: CGPoint, touchX: CGFloat) -> CGFloat { + static func distanceToTouch(from: CGPoint, to: CGPoint, touchX: CGFloat) -> CGFloat { distance(from: from, to: relativePoint(from: from, to: to, touchX: touchX)) } @@ -204,7 +207,7 @@ extension CTLineChartDataProtocol { - to: Second point - Returns: Distance between two points. */ - func distance(from: CGPoint, to: CGPoint) -> CGFloat { + 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)) } @@ -221,7 +224,7 @@ extension CTLineChartDataProtocol { - path: Path to find location on. - Returns: Point on path. */ - func locationOnPath(_ percent: CGFloat, _ path: Path) -> CGPoint { + static func locationOnPath(_ percent: CGFloat, _ path: Path) -> CGPoint { // percent difference between points let diff: CGFloat = 0.001 let comp: CGFloat = 1 - diff @@ -259,20 +262,24 @@ extension CTLineChartDataProtocol { lineColour: style.lineColour, lineWidth: style.lineWidth) .frame(width: style.size, height: style.size) - .position(self.getIndicatorLocation(rect: chartSize, + .position(Self.getIndicatorLocation(rect: chartSize, dataPoints: dataPoints, touchLocation: touchLocation, - lineType: lineType)) + lineType: lineType, + minValue: self.minValue, + range: self.range)) case .vertical(attachment: let attach): switch attach { case .line(dot: let indicator): - let position = self.getIndicatorLocation(rect: chartSize, + let position = Self.getIndicatorLocation(rect: chartSize, dataPoints: dataPoints, touchLocation: touchLocation, - lineType: lineType) + lineType: lineType, + minValue: self.minValue, + range: self.range) Vertical(position: position) .stroke(Color.primary, lineWidth: 2) @@ -293,10 +300,12 @@ extension CTLineChartDataProtocol { switch attach { case .line(dot: let indicator): - let position = self.getIndicatorLocation(rect: chartSize, + let position = Self.getIndicatorLocation(rect: chartSize, dataPoints: dataPoints, touchLocation: touchLocation, - lineType: lineType) + lineType: lineType, + minValue: self.minValue, + range: self.range) MarkerFull(position: position) .stroke(Color.primary, lineWidth: 2) @@ -319,10 +328,12 @@ extension CTLineChartDataProtocol { switch attach { case .line(dot: let indicator): - let position = self.getIndicatorLocation(rect: chartSize, + let position = Self.getIndicatorLocation(rect: chartSize, dataPoints: dataPoints, touchLocation: touchLocation, - lineType: lineType) + lineType: lineType, + minValue: self.minValue, + range: self.range) MarkerBottomLeading(position: position) .stroke(Color.primary, lineWidth: 2) @@ -345,10 +356,12 @@ extension CTLineChartDataProtocol { switch attach { case .line(dot: let indicator): - let position = self.getIndicatorLocation(rect: chartSize, + let position = Self.getIndicatorLocation(rect: chartSize, dataPoints: dataPoints, touchLocation: touchLocation, - lineType: lineType) + lineType: lineType, + minValue: self.minValue, + range: self.range) MarkerBottomTrailing(position: position) .stroke(Color.primary, lineWidth: 2) @@ -371,10 +384,12 @@ extension CTLineChartDataProtocol { switch attach { case .line(dot: let indicator): - let position = self.getIndicatorLocation(rect: chartSize, + let position = Self.getIndicatorLocation(rect: chartSize, dataPoints: dataPoints, touchLocation: touchLocation, - lineType: lineType) + lineType: lineType, + minValue: self.minValue, + range: self.range) MarkerTopLeading(position: position) .stroke(Color.primary, lineWidth: 2) @@ -397,10 +412,12 @@ extension CTLineChartDataProtocol { switch attach { case .line(dot: let indicator): - let position = self.getIndicatorLocation(rect: chartSize, + let position = Self.getIndicatorLocation(rect: chartSize, dataPoints: dataPoints, touchLocation: touchLocation, - lineType: lineType) + lineType: lineType, + minValue: self.minValue, + range: self.range) MarkerTopTrailing(position: position) .stroke(Color.primary, lineWidth: 2) diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift index 7e57c83b..5f816e3d 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift @@ -90,12 +90,3 @@ extension DoughnutChartData { return nil } } - -// MARK: - Legends -extension DoughnutChartData: LegendProtocol { - public func setupLegends() {} - - public func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } -} diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift index 31354d31..59e4e093 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift @@ -137,12 +137,3 @@ extension MultiLayerPieChartData { return nil } } - -// MARK: - Legends -extension MultiLayerPieChartData: LegendProtocol { - public func setupLegends() {} - - public func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } -} diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift index 69bde6f4..84f70183 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift @@ -91,23 +91,3 @@ extension PieChartData { return nil } } - -// MARK: - Legends -extension PieChartData: LegendProtocol { - public func setupLegends() { - for data in dataSets.dataPoints { - if let legend = data.pointDescription { - self.legends.append(LegendData(id : data.id, - legend : legend, - colour : ColourStyle(colour: data.colour), - strokeStyle: nil, - prioity : 1, - chartType : .pie)) - } - } - } - - public func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} - } -} diff --git a/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift b/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift index f272232b..662c8d37 100644 --- a/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift +++ b/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift @@ -37,7 +37,8 @@ public struct PieDataSet: CTSingleDataSetProtocol { 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 index 85c0cf50..d7c79706 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift @@ -17,6 +17,22 @@ public protocol CTPieDoughnutChartDataProtocol: CTChartData {} A protocol to extend functionality of `CTPieDoughnutChartDataProtocol` specifically for Pie Charts. */ public protocol CTPieChartDataProtocol : CTPieDoughnutChartDataProtocol {} +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.pointDescription { + self.legends.append(LegendData(id : data.id, + legend : legend, + colour : ColourStyle(colour: data.colour), + strokeStyle: nil, + prioity : 1, + chartType : .pie)) + } + } + } +} /** A protocol to extend functionality of `CTPieDoughnutChartDataProtocol` specifically for Doughnut Charts. @@ -27,7 +43,9 @@ public protocol CTDoughnutChartDataProtocol : CTPieDoughnutChartDataProtocol {} A protocol to extend functionality of `CTPieDoughnutChartDataProtocol` specifically for multi layer Pie Charts. */ public protocol CTMultiPieChartDataProtocol : CTPieDoughnutChartDataProtocol {} - +extension CTMultiPieChartDataProtocol { + internal func setupLegends() {} +} @@ -60,9 +78,11 @@ public protocol CTPieDataPoint: CTChartDataPointProtocol { The data points value in radians. */ var amount : Double { get set } + + var colour : Color { get set } } -public protocol CTMultiPieChartDataPoint: CTChartDataPointProtocol { +public protocol CTMultiPieChartDataPoint: CTPieDataPoint { /** Second layer of data points. diff --git a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift index 28bc2d52..0c12e65f 100644 --- a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift @@ -28,6 +28,7 @@ import SwiftUI .legends(chartData: data) ``` */ +// .stroke -- REMOVE FORCE UNWRAP public struct DoughnutChart: View where ChartData: DoughnutChartData { @ObservedObject var chartData: ChartData diff --git a/Sources/SwiftUICharts/PieChart/Views/MultiLayerPieChart.swift b/Sources/SwiftUICharts/PieChart/Views/MultiLayerPieChart.swift index 894a2743..3226bd74 100644 --- a/Sources/SwiftUICharts/PieChart/Views/MultiLayerPieChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/MultiLayerPieChart.swift @@ -28,6 +28,7 @@ import SwiftUI .legends(chartData: data) ``` */ +// .fill -- REMOVE FORCE UNWRAP public struct MultiLayerPieChart: View where ChartData: MultiLayerPieChartData { @ObservedObject var chartData: ChartData diff --git a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift index bfddc934..3250a560 100644 --- a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift @@ -41,6 +41,7 @@ public struct PieChart: View where ChartData: PieChartData { @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, @@ -60,6 +61,7 @@ public struct PieChart: View where ChartData: PieChartData { .accessibilityValue(Text(String(format: chartData.infoView.touchSpecifier, chartData.dataSets.dataPoints[data].value) + "\(chartData.dataSets.dataPoints[data].pointDescription ?? "")")) } } + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true } diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index fbd2f3d9..fd112c9e 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -125,31 +125,6 @@ public protocol CTChartData: ObservableObject, Identifiable { func getPointLocation(dataSet: SetPoint, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? } - - - - -// MARK: - Legend Protocol -/** - Protocol for dealing with legend data internally. - */ -public protocol LegendProtocol { - - /** - Sets the order the Legends are layed out in. - - Returns: Ordered array of Legends. - */ - func legendOrder() -> [LegendData] - - /** - Configures the legends based on the type of chart. - */ - func setupLegends() -} - - - - // MARK: - Data Sets /** Main protocol to set conformace for types of Data Sets. diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift index 5c01056e..b04a924c 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift @@ -31,3 +31,8 @@ extension CTChartData { self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) } } +extension CTChartData { + public func legendOrder() -> [LegendData] { + return legends.sorted { $0.prioity < $1.prioity} + } +} diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift index c86b673e..98c1e70f 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift @@ -30,7 +30,7 @@ extension CTLineBarChartDataProtocol where Set: CTSingleDataSetProtocol { _highestValue = max(DataFunctions.dataSetMaxValue(from: dataSets), value) } - return _highestValue - _lowestValue + return (_highestValue - _lowestValue) + 0.001 } public var minValue : Double { @@ -82,7 +82,7 @@ extension CTLineBarChartDataProtocol where Set: CTMultiDataSetProtocol { _highestValue = max(DataFunctions.multiDataSetMaxValue(from: dataSets), value) } - return _highestValue - _lowestValue + return (_highestValue - _lowestValue) + 0.001 } public var minValue : Double { switch self.chartStyle.baseline { diff --git a/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift index 86e9507b..86d77418 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/BarChartTests.swift @@ -43,21 +43,33 @@ final class BarChartTests: XCTestCase { // MARK: - Labels func testBarGetYLabels() { - let dataPoints = [ - BarChartDataPoint(value: 10), - BarChartDataPoint(value: 50), - BarChartDataPoint(value: 40), - BarChartDataPoint(value: 80) - ] - let chartData = BarChartData(dataSets: BarDataSet(dataPoints: dataPoints), - chartStyle: BarChartStyle(yAxisNumberOfLabels: 3)) - XCTAssertEqual(chartData.getYLabels()[0], 0.00000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[1], 26.6666, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[2], 53.3333, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[3], 80.0000, accuracy: 0.01) + 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) diff --git a/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift index 34ca12f0..4fa04aa7 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift @@ -13,13 +13,13 @@ final class GroupedBarChartTests: XCTestCase { var data : GroupingData { switch self { case .one: - return GroupingData(title: "One" , colour: .blue) + return GroupingData(title: "One" , fillColour: ColourStyle(colour: .blue)) case .two: - return GroupingData(title: "Two" , colour: .red) + return GroupingData(title: "Two" , fillColour: ColourStyle(colour: .red)) case .three: - return GroupingData(title: "Three", colour: .yellow) + return GroupingData(title: "Three", fillColour: ColourStyle(colour: .yellow)) case .four: - return GroupingData(title: "Four" , colour: .green) + return GroupingData(title: "Four" , fillColour: ColourStyle(colour: .green)) } } } diff --git a/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift index 5f775575..e31d6b56 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift @@ -13,13 +13,13 @@ final class StackedBarChartTests: XCTestCase { var data : GroupingData { switch self { case .one: - return GroupingData(title: "One" , colour: .blue) + return GroupingData(title: "One" , fillColour: ColourStyle(colour: .blue)) case .two: - return GroupingData(title: "Two" , colour: .red) + return GroupingData(title: "Two" , fillColour: ColourStyle(colour: .red)) case .three: - return GroupingData(title: "Three", colour: .yellow) + return GroupingData(title: "Three", fillColour: ColourStyle(colour: .yellow)) case .four: - return GroupingData(title: "Four" , colour: .green) + return GroupingData(title: "Four" , fillColour: ColourStyle(colour: .green)) } } } diff --git a/Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift b/Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift index 9c2d78b1..76f71674 100644 --- a/Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift +++ b/Tests/SwiftUIChartsTests/LineCharts/LineChartPathTests.swift @@ -17,10 +17,12 @@ final class LineChartPathTests: XCTestCase { func testGetIndicatorLocation() { - let test = chartData.getIndicatorLocation(rect: rect, + let test = LineChartData.getIndicatorLocation(rect: rect, dataPoints: chartData.dataSets.dataPoints, touchLocation: touchLocation, - lineType: .line) + lineType: .line, + minValue: chartData.minValue, + range: chartData.range) XCTAssertEqual(test.x, 25, accuracy: 0.1) XCTAssertEqual(test.y, 75, accuracy: 0.1) @@ -35,7 +37,7 @@ final class LineChartPathTests: XCTestCase { range : chartData.range, isFilled : false) - let test = chartData.getPercentageOfPath(path: path, touchLocation: touchLocation) + let test = LineChartData.getPercentageOfPath(path: path, touchLocation: touchLocation) XCTAssertEqual(test, 0.25, accuracy: 0.1) } @@ -48,7 +50,7 @@ final class LineChartPathTests: XCTestCase { range : chartData.range, isFilled : false) - let test = chartData.getTotalLength(of: path) + let test = LineChartData.getTotalLength(of: path) XCTAssertEqual(test, 141.42, accuracy: 0.01) } @@ -61,7 +63,7 @@ final class LineChartPathTests: XCTestCase { range : chartData.range, isFilled : false) - let test = chartData.getLength(to: touchLocation, on: path) + let test = LineChartData.getLength(to: touchLocation, on: path) XCTAssertEqual(test, 35.35, accuracy: 0.01) } @@ -71,7 +73,7 @@ final class LineChartPathTests: XCTestCase { let pointOne = CGPoint(x: 0.0, y: 0.0) let pointTwo = CGPoint(x: 100, y: 100) - let test = chartData.relativePoint(from: pointOne, to: pointTwo, touchX: touchLocation.x) + 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) @@ -82,7 +84,7 @@ final class LineChartPathTests: XCTestCase { let pointOne = CGPoint(x: 0.0, y: 0.0) let pointTwo = CGPoint(x: 100, y: 100) - let test = chartData.distanceToTouch(from: pointOne, to: pointTwo, touchX: touchLocation.x) + let test = LineChartData.distanceToTouch(from: pointOne, to: pointTwo, touchX: touchLocation.x) XCTAssertEqual(test, 35.355, accuracy: 0.01) } @@ -92,7 +94,7 @@ final class LineChartPathTests: XCTestCase { let pointOne = CGPoint(x: 0.0, y: 0.0) let pointTwo = CGPoint(x: 100, y: 100) - let test = chartData.distance(from: pointOne, to: pointTwo) + let test = LineChartData.distance(from: pointOne, to: pointTwo) XCTAssertEqual(test, 141.421356237309, accuracy: 0.01) } @@ -106,7 +108,7 @@ final class LineChartPathTests: XCTestCase { isFilled : false) - let test = chartData.locationOnPath(0.5, path) + let test = LineChartData.locationOnPath(0.5, path) XCTAssertEqual(test.x, 50, accuracy: 0.1) XCTAssertEqual(test.y, 50, accuracy: 0.1) From 06298e6f42c91043efbb114e1adeae6bba31cd69 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 3 Mar 2021 11:33:16 +0000 Subject: [PATCH 113/152] Tidy up markerSubView. --- .../Models/ChartData/BarChartData.swift | 36 +------- .../ChartData/GroupedBarChartData.swift | 92 +++++++------------ .../ChartData/StackedBarChartData.swift | 83 ++++++----------- .../Models/Protocols/BarChartProtocols.swift | 48 +++++++++- .../Models/ChartData/LineChartData.swift | 3 +- .../Models/ChartData/MultiLineChartData.swift | 3 +- .../ChartData/RangedLineChartData.swift | 3 +- .../LineChartProtocolsExtensions.swift | 19 ++-- .../Models/Protocols/SharedProtocols.swift | 2 - 9 files changed, 118 insertions(+), 171 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index bc04f961..05ef6f5d 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -131,42 +131,10 @@ public final class BarChartData: CTBarChartDataProtocol { } } - - - @ViewBuilder public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { - - if let position = self.getPointLocation(dataSet: dataSets, - touchLocation: touchLocation, - chartSize: chartSize) { - - ZStack { - 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) - } - } - } else { EmptyView() } + self.markerSubView(dataSet: dataSets, touchLocation: touchLocation, chartSize: chartSize) } - + public typealias Set = BarDataSet public typealias DataPoint = BarChartDataPoint public typealias CTStyle = BarChartStyle diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 8575bf08..2da93140 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -129,88 +129,58 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { } // MARK: Labels - @ViewBuilder public func getXAxisLabels() -> some View { - switch self.chartStyle.xAxisLabelsFrom { - case .dataPoint: - HStack(spacing: self.groupSpacing) { - ForEach(dataSets.dataSets) { dataSet in + Group { + switch self.chartStyle.xAxisLabelsFrom { + case .dataPoint: + HStack(spacing: self.groupSpacing) { + ForEach(dataSets.dataSets) { dataSet in + HStack(spacing: 0) { + ForEach(dataSet.dataPoints) { data in + Spacer() + .frame(minWidth: 0, maxWidth: 500) + Text(data.xAxisLabel ?? "") + .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) + .lineLimit(1) + .minimumScaleFactor(0.5) + .accessibilityLabel( Text("XAxisLabel")) + .accessibilityValue(Text("\(data.xAxisLabel ?? "")")) + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } + } + .padding(.horizontal, -4) + + case .chartData: + + if let labelArray = self.xAxisLabels { HStack(spacing: 0) { - ForEach(dataSet.dataPoints) { data in + ForEach(labelArray, id: \.self) { data in Spacer() .frame(minWidth: 0, maxWidth: 500) - Text(data.xAxisLabel ?? "") + Text(data) .font(.caption) .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) .accessibilityLabel( Text("XAxisLabel")) - .accessibilityValue(Text("\(data.xAxisLabel ?? "")")) + .accessibilityValue(Text("\(data)")) 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) - Text(data) - .font(.caption) - .foregroundColor(self.chartStyle.xAxisLabelColour) - .lineLimit(1) - .minimumScaleFactor(0.5) - .accessibilityLabel( Text("XAxisLabel")) - .accessibilityValue(Text("\(data)")) - Spacer() - .frame(minWidth: 0, maxWidth: 500) - } - } - } } } - @ViewBuilder public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { - - if let position = self.getPointLocation(dataSet: dataSets, - touchLocation: touchLocation, - chartSize: chartSize) { - ZStack { - - 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) - } - } - } else { EmptyView() } + self.markerSubView(dataSet: dataSets, touchLocation: touchLocation, chartSize: chartSize) } - public typealias Set = MultiBarDataSets public typealias DataPoint = MultiBarChartDataPoint public typealias CTStyle = BarChartStyle diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index a9871870..0116c681 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -122,79 +122,50 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { self.setupLegends() } // MARK: Labels - @ViewBuilder public func getXAxisLabels() -> some View { - switch self.chartStyle.xAxisLabelsFrom { - case .dataPoint: - HStack(spacing: 0) { - ForEach(groups) { group in - Spacer() - .frame(minWidth: 0, maxWidth: 500) - Text(group.title) - .font(.caption) - .foregroundColor(self.chartStyle.xAxisLabelColour) - .lineLimit(1) - .minimumScaleFactor(0.5) - .accessibilityLabel( Text("X Axis Label")) - .accessibilityValue(Text("\(group.title)")) - - Spacer() - .frame(minWidth: 0, maxWidth: 500) - } - } - case .chartData: - if let labelArray = self.xAxisLabels { + Group { + switch self.chartStyle.xAxisLabelsFrom { + case .dataPoint: HStack(spacing: 0) { - ForEach(labelArray, id: \.self) { data in + ForEach(groups) { group in Spacer() .frame(minWidth: 0, maxWidth: 500) - Text(data) + Text(group.title) .font(.caption) .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) .accessibilityLabel( Text("X Axis Label")) - .accessibilityValue(Text("\(data)")) + .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) + Text(data) + .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) + .lineLimit(1) + .minimumScaleFactor(0.5) + .accessibilityLabel( Text("X Axis Label")) + .accessibilityValue(Text("\(data)")) + Spacer() + .frame(minWidth: 0, maxWidth: 500) + } + } + } } } } - - @ViewBuilder + public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { - - if let position = self.getPointLocation(dataSet: dataSets, - touchLocation: touchLocation, - chartSize: chartSize) { - ZStack { - - 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) - } - } - } else { EmptyView() } + self.markerSubView(dataSet: dataSets, touchLocation: touchLocation, chartSize: chartSize) } public typealias Set = MultiBarDataSets diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index 877be885..74f31a1e 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -20,6 +20,52 @@ public protocol CTBarChartDataProtocol: CTLineBarChartDataProtocol { var barStyle : BarStyle { get set } } +extension CTBarChartDataProtocol where Self.CTStyle.Mark == BarMarkerType { + internal func markerSubView + (dataSet : DS, + touchLocation : CGPoint, + chartSize : CGRect) -> some View { + Group { + if let position = self.getPointLocation(dataSet: dataSets as! Self.SetPoint, + touchLocation: touchLocation, + chartSize: 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) + } + } + } + } +} + + extension CTBarChartDataProtocol where Self.Set.ID == UUID, Self.Set.DataPoint.ID == UUID, Self.Set: CTStandardBarChartDataSet, @@ -204,8 +250,6 @@ public protocol CTMultiBarChartDataSet: CTSingleDataSetProtocol {} - - // MARK: - DataPoints /** A protocol to extend functionality of `CTLineBarDataPointProtocol` specifically for standard Bar Charts. diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index 5398fc80..a786a8bb 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -147,8 +147,7 @@ public final class LineChartData: CTLineChartDataProtocol { } public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { - self.markerSubView(markerType: self.chartStyle.markerType, - dataSet: dataSets, + self.markerSubView(dataSet: dataSets, dataPoints: dataSets.dataPoints, lineType: dataSets.style.lineType, touchLocation: touchLocation, diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index 092c9d20..2ce375eb 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -158,8 +158,7 @@ public final class MultiLineChartData: CTLineChartDataProtocol { public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { ZStack { ForEach(self.dataSets.dataSets, id: \.self) { dataSet in - self.markerSubView(markerType: self.chartStyle.markerType, - dataSet: dataSet, + self.markerSubView(dataSet: dataSet, dataPoints: dataSet.dataPoints, lineType: dataSet.style.lineType, touchLocation: touchLocation, diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift index 27021337..b409220b 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift @@ -147,8 +147,7 @@ public final class RangedLineChartData: CTLineChartDataProtocol { } public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { - self.markerSubView(markerType: self.chartStyle.markerType, - dataSet: dataSets, + self.markerSubView(dataSet: dataSets, dataPoints: dataSets.dataPoints, lineType: dataSets.style.lineType, touchLocation: touchLocation, diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index 61e2fe1a..b2d3baaf 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -242,18 +242,17 @@ extension CTLineChartDataProtocol { } // MARK: - Markers -extension CTLineChartDataProtocol { +extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { - public func markerSubView - (markerType : LineMarkerType, - dataSet : DS, + internal func markerSubView + (dataSet : DS, dataPoints : [DP], lineType : LineType, touchLocation : CGPoint, chartSize : CGRect) -> some View { Group { - switch markerType { + switch self.chartStyle.markerType { case .none: EmptyView() case .indicator(let style): @@ -312,7 +311,7 @@ extension CTLineChartDataProtocol { IndicatorSwitch(indicator: indicator, location: position) - case .point: EmptyView() + case .point: if let position = self.getPointLocation(dataSet: dataSet as! Self.SetPoint, touchLocation: touchLocation, @@ -340,7 +339,7 @@ extension CTLineChartDataProtocol { IndicatorSwitch(indicator: indicator, location: position) - case .point: EmptyView() + case .point: if let position = self.getPointLocation(dataSet: dataSet as! Self.SetPoint, touchLocation: touchLocation, @@ -368,7 +367,7 @@ extension CTLineChartDataProtocol { IndicatorSwitch(indicator: indicator, location: position) - case .point: EmptyView() + case .point: if let position = self.getPointLocation(dataSet: dataSet as! Self.SetPoint, touchLocation: touchLocation, @@ -396,7 +395,7 @@ extension CTLineChartDataProtocol { IndicatorSwitch(indicator: indicator, location: position) - case .point: EmptyView() + case .point: if let position = self.getPointLocation(dataSet: dataSet as! Self.SetPoint, touchLocation: touchLocation, diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index fd112c9e..4afd53c6 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -70,8 +70,6 @@ public protocol CTChartData: ObservableObject, Identifiable { Holds data about the charts type. Allows for internal logic based on the type of chart. - - This might get removed in favour of a more protocol based approach. */ var chartType: (chartType: ChartType, dataSetType: DataSetType) { get } From 96bacb032432c5718c54d036dfa2c031861a9253 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 3 Mar 2021 12:19:04 +0000 Subject: [PATCH 114/152] Refactor Bar Chart Markers. --- .../Models/ChartData/BarChartData.swift | 19 +- .../ChartData/GroupedBarChartData.swift | 16 +- .../ChartData/StackedBarChartData.swift | 176 +++++++++-------- .../Models/Protocols/BarChartProtocols.swift | 169 +---------------- .../BarChartProtocolsExtensions.swift | 179 ++++++++++++++++++ 5 files changed, 279 insertions(+), 280 deletions(-) create mode 100644 Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 05ef6f5d..dd07736e 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -130,19 +130,11 @@ public final class BarChartData: CTBarChartDataProtocol { } } } - + + // MARK: - Touch public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { self.markerSubView(dataSet: dataSets, touchLocation: touchLocation, chartSize: chartSize) } - - public typealias Set = BarDataSet - public typealias DataPoint = BarChartDataPoint - public typealias CTStyle = BarChartStyle -} - -// MARK: - Touch -extension BarChartData { - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [BarChartDataPoint] = [] let xSection : CGFloat = chartSize.width / CGFloat(dataSets.dataPoints.count) @@ -152,7 +144,6 @@ extension BarChartData { } self.infoView.touchOverlayInfo = points } - public 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) @@ -163,4 +154,10 @@ extension BarChartData { } return nil } + + public typealias Set = BarDataSet + public typealias DataPoint = BarChartDataPoint + public typealias CTStyle = BarChartStyle } + + diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 2da93140..ab41f6a5 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -176,19 +176,10 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { } } } - + // MARK: Touch public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { self.markerSubView(dataSet: dataSets, touchLocation: touchLocation, chartSize: chartSize) } - - public typealias Set = MultiBarDataSets - public typealias DataPoint = MultiBarChartDataPoint - public typealias CTStyle = BarChartStyle -} - -// MARK: - Touch -extension GroupedBarChartData { - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [MultiBarChartDataPoint] = [] @@ -214,7 +205,6 @@ extension GroupedBarChartData { } self.infoView.touchOverlayInfo = points } - public func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { // Divide the chart into equal sections. @@ -246,4 +236,8 @@ extension GroupedBarChartData { } return nil } + + public typealias Set = MultiBarDataSets + public typealias DataPoint = MultiBarChartDataPoint + public typealias CTStyle = BarChartStyle } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index 0116c681..50600300 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -163,109 +163,105 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { } } } - + // MARK: Touch public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { self.markerSubView(dataSet: dataSets, touchLocation: touchLocation, chartSize: chartSize) } + + public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { - public typealias Set = MultiBarDataSets - public typealias DataPoint = MultiBarChartDataPoint - public typealias CTStyle = BarChartStyle -} + 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) -// MARK: - Touch -extension StackedBarChartData { - - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + // 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 { + points.append(dataSet.dataPoints[index]) + } + } + } + self.infoView.touchOverlayInfo = points + } - 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) + public 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) - // 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 { - points.append(dataSet.dataPoints[index]) - } - } - } - self.infoView.touchOverlayInfo = points - } - - public 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 { - if superIndex >= 0 && superIndex < dataSet.dataSets.count { + let subDataSet = dataSet.dataSets[superIndex] - 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) - // 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 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) + } - // 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) + }) - 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 { - 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 + } - 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/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index 74f31a1e..994b1ba6 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -20,135 +20,7 @@ public protocol CTBarChartDataProtocol: CTLineBarChartDataProtocol { var barStyle : BarStyle { get set } } -extension CTBarChartDataProtocol where Self.CTStyle.Mark == BarMarkerType { - internal func markerSubView - (dataSet : DS, - touchLocation : CGPoint, - chartSize : CGRect) -> some View { - Group { - if let position = self.getPointLocation(dataSet: dataSets as! Self.SetPoint, - touchLocation: touchLocation, - chartSize: 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) - } - } - } - } -} - -extension CTBarChartDataProtocol where Self.Set.ID == UUID, - Self.Set.DataPoint.ID == UUID, - Self.Set: CTStandardBarChartDataSet, - Self.Set.DataPoint: CTStandardBarDataPoint { - internal func setupLegends() { - - switch self.barStyle.colourFrom { - case .barStyle: - if self.barStyle.fillColour.colourType == .colour, - let colour = self.barStyle.fillColour.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.fillColour.colourType == .gradientColour, - let colours = self.barStyle.fillColour.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.fillColour.colourType == .gradientStops, - let stops = self.barStyle.fillColour.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.fillColour.colourType == .colour, - let colour = data.fillColour.colour, - let legend = data.pointDescription - { - self.legends.append(LegendData(id : data.id, - legend : legend, - colour : ColourStyle(colour: colour), - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if data.fillColour.colourType == .gradientColour, - let colours = data.fillColour.colours, - let legend = data.pointDescription - { - 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.fillColour.colourType == .gradientStops, - let stops = data.fillColour.stops, - let legend = data.pointDescription - { - self.legends.append(LegendData(id : data.id, - legend : legend, - colour : ColourStyle(stops: stops, - startPoint: .leading, - endPoint: .trailing), - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } - } - } - } -} /** A protocol to extend functionality of `CTBarChartDataProtocol` specifically for Multi Part Bar Charts. @@ -161,46 +33,7 @@ public protocol CTMultiBarChartDataProtocol: CTBarChartDataProtocol { var groups : [GroupingData] { get set } } -extension CTMultiBarChartDataProtocol { - internal func setupLegends() { - - for group in self.groups { - - if group.fillColour.colourType == .colour, - let colour = group.fillColour.colour - { - self.legends.append(LegendData(id : group.id, - legend : group.title, - colour : ColourStyle(colour: colour), - strokeStyle: nil, - prioity : 1, - chartType : .bar)) - } else if group.fillColour.colourType == .gradientColour, - let colours = group.fillColour.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.fillColour.colourType == .gradientStops, - let stops = group.fillColour.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/Protocols/BarChartProtocolsExtensions.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift new file mode 100644 index 00000000..5d337750 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift @@ -0,0 +1,179 @@ +// +// BarChartProtocolsExtensions.swift +// +// +// Created by Will Dale on 03/03/2021. +// + +import SwiftUI + +extension CTBarChartDataProtocol where Self.CTStyle.Mark == BarMarkerType { + internal func markerSubView + (dataSet : DS, + touchLocation : CGPoint, + chartSize : CGRect) -> some View { + Group { + if let position = self.getPointLocation(dataSet: dataSets as! Self.SetPoint, + touchLocation: touchLocation, + chartSize: 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) + } + } + } + } +} + + +extension CTBarChartDataProtocol where Self.Set.ID == UUID, + Self.Set.DataPoint.ID == UUID, + Self.Set: CTStandardBarChartDataSet, + Self.Set.DataPoint: CTStandardBarDataPoint { + internal func setupLegends() { + + switch self.barStyle.colourFrom { + case .barStyle: + if self.barStyle.fillColour.colourType == .colour, + let colour = self.barStyle.fillColour.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.fillColour.colourType == .gradientColour, + let colours = self.barStyle.fillColour.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.fillColour.colourType == .gradientStops, + let stops = self.barStyle.fillColour.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.fillColour.colourType == .colour, + let colour = data.fillColour.colour, + let legend = data.pointDescription + { + self.legends.append(LegendData(id : data.id, + legend : legend, + colour : ColourStyle(colour: colour), + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if data.fillColour.colourType == .gradientColour, + let colours = data.fillColour.colours, + let legend = data.pointDescription + { + 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.fillColour.colourType == .gradientStops, + let stops = data.fillColour.stops, + let legend = data.pointDescription + { + self.legends.append(LegendData(id : data.id, + legend : legend, + colour : ColourStyle(stops: stops, + startPoint: .leading, + endPoint: .trailing), + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } + } + } + } +} + +extension CTMultiBarChartDataProtocol { + internal func setupLegends() { + + for group in self.groups { + + if group.fillColour.colourType == .colour, + let colour = group.fillColour.colour + { + self.legends.append(LegendData(id : group.id, + legend : group.title, + colour : ColourStyle(colour: colour), + strokeStyle: nil, + prioity : 1, + chartType : .bar)) + } else if group.fillColour.colourType == .gradientColour, + let colours = group.fillColour.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.fillColour.colourType == .gradientStops, + let stops = group.fillColour.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)) + } + } + } +} From 3ba3c3bc7ec6939515e44448e2a519481989ccda Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 3 Mar 2021 12:22:54 +0000 Subject: [PATCH 115/152] Remove Multi layered pie chart. --- .../Models/Protocols/PieChartProtocols.swift | 44 +---------- .../PieChartProtocolsExtentions.swift | 77 ++++--------------- 2 files changed, 18 insertions(+), 103 deletions(-) diff --git a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift index d7c79706..84fc2242 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift @@ -17,50 +17,14 @@ public protocol CTPieDoughnutChartDataProtocol: CTChartData {} A protocol to extend functionality of `CTPieDoughnutChartDataProtocol` specifically for Pie Charts. */ public protocol CTPieChartDataProtocol : CTPieDoughnutChartDataProtocol {} -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.pointDescription { - self.legends.append(LegendData(id : data.id, - legend : legend, - colour : ColourStyle(colour: data.colour), - strokeStyle: nil, - prioity : 1, - chartType : .pie)) - } - } - } -} /** A protocol to extend functionality of `CTPieDoughnutChartDataProtocol` specifically for Doughnut Charts. */ public protocol CTDoughnutChartDataProtocol : CTPieDoughnutChartDataProtocol {} -/** - A protocol to extend functionality of `CTPieDoughnutChartDataProtocol` specifically for multi layer Pie Charts. - */ -public protocol CTMultiPieChartDataProtocol : CTPieDoughnutChartDataProtocol {} -extension CTMultiPieChartDataProtocol { - internal func setupLegends() {} -} - - - -// MARK: - DataSet -public protocol CTMultiPieDataSet: CTDataSetProtocol {} - - - - - - - // MARK: - DataPoints - /** A protocol to extend functionality of `CTChartDataPointProtocol` specifically for Pie and Doughnut Charts. */ @@ -82,13 +46,7 @@ public protocol CTPieDataPoint: CTChartDataPointProtocol { var colour : Color { get set } } -public protocol CTMultiPieChartDataPoint: CTPieDataPoint { - - /** - Second layer of data points. - */ - var layerDataPoints : [MultiPieDataPoint]? { get set } -} + diff --git a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift index b31630fc..6930b8bb 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift @@ -8,66 +8,6 @@ import SwiftUI // MARK: - Extentions -extension CTPieDoughnutChartDataProtocol where Set == MultiPieDataSet, DataPoint == MultiPieDataPoint { - /** - 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].startAngle = startAngle - self.dataSets.dataPoints[point].amount = amount - - - let layerTotal = self.dataSets.dataPoints[point].layerDataPoints?.reduce(0) { $0 + $1.value } ?? 0 - var layerStartAngle = startAngle - self.dataSets.dataPoints[point].layerDataPoints?.indices.forEach { (layer) in - let layerValue = self.dataSets.dataPoints[point].layerDataPoints?[layer].value ?? 0 - let layerAmount = amount * (layerValue / layerTotal) - self.dataSets.dataPoints[point].layerDataPoints?[layer].startAngle = layerStartAngle - self.dataSets.dataPoints[point].layerDataPoints?[layer].amount = layerAmount - - - - let layerTwoTotal = self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?.reduce(0) { $0 + $1.value } ?? 0 - var layerTwoStartAngle = layerStartAngle - self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?.indices.forEach { (layerTwo) in - let layerTwoValue = self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].value ?? 0 - let layerTwoAmount = layerAmount * (layerTwoValue / layerTwoTotal) - self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].startAngle = layerTwoStartAngle - self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].amount = layerTwoAmount - - - - let layerThreeTotal = self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].layerDataPoints?.reduce(0) { $0 + $1.value } ?? 0 - var layerThreeStartAngle = layerTwoStartAngle - self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].layerDataPoints?.indices.forEach { (layerThree) in - let layerThreeValue = self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].layerDataPoints?[layerThree].value ?? 0 - let layerThreeAmount = layerTwoAmount * (layerThreeValue / layerThreeTotal) - self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].layerDataPoints?[layerThree].startAngle = layerThreeStartAngle - self.dataSets.dataPoints[point].layerDataPoints?[layer].layerDataPoints?[layerTwo].layerDataPoints?[layerThree].amount = layerThreeAmount - - layerThreeStartAngle += layerThreeAmount - } - - - - layerTwoStartAngle += layerTwoAmount - } - - - layerStartAngle += layerAmount - } - - startAngle += amount - } - } -} extension CTPieDoughnutChartDataProtocol where Set == PieDataSet, DataPoint == PieChartDataPoint { @@ -113,3 +53,20 @@ extension CTPieDoughnutChartDataProtocol where Set == PieDataSet, DataPoint == P } } } + +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.pointDescription { + self.legends.append(LegendData(id : data.id, + legend : legend, + colour : ColourStyle(colour: data.colour), + strokeStyle: nil, + prioity : 1, + chartType : .pie)) + } + } + } +} From 48bc6aa07addd9a0c931cf28a70d43c521b566de Mon Sep 17 00:00:00 2001 From: Will Dale Date: Wed, 3 Mar 2021 12:23:01 +0000 Subject: [PATCH 116/152] Remove Multi layered pie chart. --- .../ChartData/MultiLayerPieChartData.swift | 139 ------------------ .../Models/DataPoints/MultiPieDataPoint.swift | 56 ------- .../Models/DataSets/MultiPieDataSet.swift | 39 ----- .../PieChart/Views/MultiLayerPieChart.swift | 90 ------------ 4 files changed, 324 deletions(-) delete mode 100644 Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift delete mode 100644 Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift delete mode 100644 Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift delete mode 100644 Sources/SwiftUICharts/PieChart/Views/MultiLayerPieChart.swift diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift deleted file mode 100644 index 59e4e093..00000000 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/MultiLayerPieChartData.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// MultiLayerPieChartData.swift -// -// -// Created by Will Dale on 05/02/2021. -// - -import SwiftUI - -/** - Data for drawing and styling a multi layered pie chart. - - This model contains the data and styling information for a multi layered pie chart - - # Example - ``` - public static func makeData() -> MultiLayerPieChartData { - - let data = MultiPieDataSet(dataPoints: [ - MultiPieDataPoint(value: 40, pointDescription: "One", colour: Color(.red), - layerDataPoints: [ - MultiPieDataPoint(value: 50, colour: Color(.cyan), - layerDataPoints: [ - MultiPieDataPoint(value: 70, colour: .red, - layerDataPoints: [ - MultiPieDataPoint(value: 20, colour: .red), - MultiPieDataPoint(value: 30, colour: .blue) - ]), - MultiPieDataPoint(value: 30, colour: .blue, - layerDataPoints: [ - MultiPieDataPoint(value: 30, colour: .green), - MultiPieDataPoint(value: 50, colour: .orange) - ]) - ]), - MultiPieDataPoint(value: 70, colour: Color(.yellow), - layerDataPoints: [ - MultiPieDataPoint(value: 50, colour: .green, - layerDataPoints: [ - MultiPieDataPoint(value: 30, colour: .yellow), - MultiPieDataPoint(value: 30, colour: .pink) - ]), - MultiPieDataPoint(value: 30, colour: .red, - layerDataPoints: [ - MultiPieDataPoint(value: 50, colour: .green), - MultiPieDataPoint(value: 20, colour: .orange) - ]) - ]) - ]), - MultiPieDataPoint(value: 40, pointDescription: "Two", colour: Color(.blue), - layerDataPoints: [ - MultiPieDataPoint(value: 50, colour: Color(.cyan), - layerDataPoints: [ - MultiPieDataPoint(value: 70, colour: .red, - layerDataPoints: [ - MultiPieDataPoint(value: 60, colour: .green), - MultiPieDataPoint(value: 40, colour: .yellow) - ]), - MultiPieDataPoint(value: 30, colour: .blue, - layerDataPoints: [ - MultiPieDataPoint(value: 30, colour: .red), - MultiPieDataPoint(value: 20, colour: .orange) - ]) - ]), - MultiPieDataPoint(value: 70, colour: Color(.green), - layerDataPoints: [ - MultiPieDataPoint(value: 50, colour: .green, - layerDataPoints: [ - MultiPieDataPoint(value: 70, colour: .green), - MultiPieDataPoint(value: 60, colour: .pink) - ]), - MultiPieDataPoint(value: 30, colour: .red, - layerDataPoints: [ - MultiPieDataPoint(value: 10, colour: .orange), - MultiPieDataPoint(value: 50, colour: .pink) - ]) - ]) - ]) - ]) - return MultiLayerPieChartData(dataSets: data, - metadata: ChartMetadata(title: "Pie", subtitle: "mmm pie"), - chartStyle: PieChartStyle(infoBoxPlacement: .header)) - } - ``` - */ -public final class MultiLayerPieChartData: CTMultiPieChartDataProtocol { - - // MARK: Properties - public var id : UUID = UUID() - @Published public var dataSets : MultiPieDataSet - @Published public var metadata : ChartMetadata - @Published public var chartStyle : PieChartStyle - @Published public var legends : [LegendData] - @Published public var infoView : InfoViewData - - public var noDataText: Text - public var chartType : (chartType: ChartType, dataSetType: DataSetType) - - // MARK: Initializer - /// Initialises a multi layered pie chart. - /// - /// - 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 : MultiPieDataSet, - 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 func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { EmptyView() } - - public typealias Set = MultiPieDataSet - public typealias DataPoint = MultiPieDataPoint - public typealias CTStyle = PieChartStyle -} - -// MARK: - Touch -extension MultiLayerPieChartData { - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { - let points : [MultiPieDataPoint] = [] - self.infoView.touchOverlayInfo = points - } - public func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { - return nil - } -} diff --git a/Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift b/Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift deleted file mode 100644 index ab40d605..00000000 --- a/Sources/SwiftUICharts/PieChart/Models/DataPoints/MultiPieDataPoint.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// MultiPieDataPoint.swift -// -// -// Created by Will Dale on 22/02/2021. -// - -import SwiftUI - -/** - Data for a single segement of a pie chart. - - # Example - ``` - MultiPieDataPoint(value: 40, pointDescription: "One", colour: Color.red, - layerDataPoints: [ - MultiPieDataPoint(value: 5, colour: Color.blue) - ]) - - ``` - */ -public struct MultiPieDataPoint: CTMultiPieChartDataPoint { - - public var id : UUID = UUID() - // CTPieDataPoint - public var startAngle : Double = 0 - public var amount : Double = 0 - // CTChartDataPointProtocol - public var value : Double - public var pointDescription : String? - public var date : Date? - // CTMultiPieChartDataPoint - public var layerDataPoints : [MultiPieDataPoint]? - - public var colour : Color - - /// Data model for a single data point for a pie chart. - /// - Parameters: - /// - value: Value of the data point - /// - 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 of the segment. - /// - layerDataPoints: Optional data points for next layer out. - public init(value : Double, - pointDescription: String? = nil, - date : Date? = nil, - colour : Color = Color.red, - layerDataPoints : [MultiPieDataPoint]? = nil - ) { - self.value = value - self.pointDescription = pointDescription - self.date = date - self.colour = colour - self.layerDataPoints = layerDataPoints - } -} diff --git a/Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift b/Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift deleted file mode 100644 index 1578b888..00000000 --- a/Sources/SwiftUICharts/PieChart/Models/DataSets/MultiPieDataSet.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// MultiPieDataSet.swift -// -// -// Created by Will Dale on 22/02/2021. -// - -import SwiftUI - -/** - Data set for drawing a multi layered pie chart. - - # Example - ``` - MultiPieDataSet(dataPoints: [ - MultiPieDataPoint(value: 30, colour: .red, layerDataPoints: [ - MultiPieDataPoint(value: 20, colour: .pink), - MultiPieDataPoint(value: 30, colour: .orange) - ]), - MultiPieDataPoint(value: 50, colour: .blue, layerDataPoints: [ - MultiPieDataPoint(value: 10, colour: .purple), - MultiPieDataPoint(value: 20, colour: .green) - ]) - ]) - ``` - */ -public struct MultiPieDataSet: CTSingleDataSetProtocol { - - public var id: UUID = UUID() - public var dataPoints : [MultiPieDataPoint] - - /// Initialises a data set a multi layered pie chart. - /// - Parameter dataPoints: Array of elements. - public init(dataPoints: [MultiPieDataPoint]) { - self.dataPoints = dataPoints - } - - public typealias DataPoint = MultiPieDataPoint -} diff --git a/Sources/SwiftUICharts/PieChart/Views/MultiLayerPieChart.swift b/Sources/SwiftUICharts/PieChart/Views/MultiLayerPieChart.swift deleted file mode 100644 index 3226bd74..00000000 --- a/Sources/SwiftUICharts/PieChart/Views/MultiLayerPieChart.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// MultiLayerPie.swift -// -// -// Created by Will Dale on 22/02/2021. -// - -import SwiftUI - -/** - View for creating a multi layer pie chart. - - Uses `MultiLayerPieChartData` data model. - - # Declaration - ``` - MultiLayerPieChart(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) - .headerBox(chartData: data) - .legends(chartData: data) - ``` - */ -// .fill -- REMOVE FORCE UNWRAP -public struct MultiLayerPieChart: View where ChartData: MultiLayerPieChartData { - - @ObservedObject var chartData: ChartData - - /// Initialises a bar chart view. - /// - Parameter chartData: Must be MultiLayerPieChartData. - public init(chartData: ChartData) { - self.chartData = chartData - } - - @State private var incept: CGFloat = 0 - - public var body: some View { - - ZStack { - ForEach(chartData.dataSets.dataPoints, id: \.self) { data in - PieSegmentShape(id: data.id, - startAngle: data.startAngle, - amount: data.amount) - .fill(data.colour) - .accessibilityLabel(Text("\(chartData.metadata.title)")) - .accessibilityValue(Text(String(format: chartData.infoView.touchSpecifier, data.value) + "\(data.pointDescription ?? "")")) - - if let points = data.layerDataPoints { - ForEach(points, id: \.self) { point in - DoughnutSegmentShape(id: point.id, - startAngle: point.startAngle, - amount: point.amount) - .strokeBorder(point.colour, lineWidth: 120) - .accessibilityLabel(Text("\(chartData.metadata.title)")) - .accessibilityValue(Text(String(format: chartData.infoView.touchSpecifier, point.value) + "\(point.pointDescription ?? "")")) - - if let pointsTwo = point.layerDataPoints { - ForEach(pointsTwo, id: \.self) { pointTwo in - DoughnutSegmentShape(id: pointTwo.id, - startAngle: pointTwo.startAngle, - amount: pointTwo.amount) - .strokeBorder(pointTwo.colour, lineWidth: 80) - .accessibilityLabel(Text("\(chartData.metadata.title)")) - .accessibilityValue(Text(String(format: chartData.infoView.touchSpecifier, pointTwo.value) + "\(pointTwo.pointDescription ?? "")")) - - if let pointsThree = pointTwo.layerDataPoints { - ForEach(pointsThree, id: \.self) { pointThree in - DoughnutSegmentShape(id: pointThree.id, - startAngle: pointThree.startAngle, - amount: pointThree.amount) - .strokeBorder(pointThree.colour, lineWidth: 40) - .accessibilityLabel(Text("\(chartData.metadata.title)")) - .accessibilityValue(Text(String(format: chartData.infoView.touchSpecifier, pointThree.value) + "\(pointThree.pointDescription ?? "")")) - } - } - } - } - } - } - } - } - } -} From 8cf046974f4763308dc6cc5e59ec3cd9111e9be1 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 5 Mar 2021 09:27:00 +0000 Subject: [PATCH 117/152] Add Ranged Bar Chart. --- .../Models/ChartData/RangedBarChartData.swift | 174 ++++++++++++++++++ .../Models/DataSet/RangedBarDataSet.swift | 30 +++ .../Datapoints/RangedBarDataPoint.swift | 46 +++++ .../Models/Protocols/BarChartProtocols.swift | 14 +- .../BarChart/Views/RangedBarChart.swift | 118 ++++++++++++ 5 files changed, 378 insertions(+), 4 deletions(-) create mode 100644 Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift create mode 100644 Sources/SwiftUICharts/BarChart/Models/DataSet/RangedBarDataSet.swift create mode 100644 Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift create mode 100644 Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift new file mode 100644 index 00000000..da7b6c70 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift @@ -0,0 +1,174 @@ +// +// 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: + + HStack(spacing: 0) { + ForEach(dataSets.dataPoints) { data in + Spacer() + .frame(minWidth: 0, maxWidth: 500) + Text(data.xAxisLabel ?? "") + .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) + .lineLimit(1) + .minimumScaleFactor(0.5) + .accessibilityLabel( Text("X Axis Label")) + .accessibilityValue(Text("\(data.xAxisLabel ?? "")")) + 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) + } + Text(data) + .font(.caption) + .foregroundColor(self.chartStyle.xAxisLabelColour) + .lineLimit(1) + .minimumScaleFactor(0.5) + .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(dataSet: dataSets, touchLocation: touchLocation, chartSize: chartSize) + } + 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 { + points.append(dataSets.dataPoints[index]) + } + 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 ySection : CGFloat = chartSize.height / CGFloat(self.maxValue) + let index : Int = Int((touchLocation.x) / xSection) + if index >= 0 && index < dataSet.dataPoints.count { + + let upperY = (chartSize.size.height - CGFloat(dataSet.dataPoints[index].upperValue) * ySection) + let lowerY = (chartSize.size.height - CGFloat(dataSet.dataPoints[index].lowerValue) * ySection) + + return CGPoint(x: (CGFloat(index) * xSection) + (xSection / 2), + y: upperY - lowerY) + } + return nil + } + public final func headerTouchOverlaySubView(info: RangedBarDataPoint) -> some View { + Group { + switch self.infoView.touchUnit { + case .none: + Text("\(info.upperValue, specifier: self.infoView.touchSpecifier)") + .font(.title3) + .foregroundColor(self.chartStyle.infoBoxValueColour) + Text("\(info.pointDescription ?? "")") + .font(.subheadline) + .foregroundColor(self.chartStyle.infoBoxDescriptionColour) + case .prefix(of: let unit): + Text("\(unit) \(info.upperValue, specifier: self.infoView.touchSpecifier)") + .font(.title3) + .foregroundColor(self.chartStyle.infoBoxValueColour) + Text("\(info.pointDescription ?? "")") + .font(.subheadline) + .foregroundColor(self.chartStyle.infoBoxDescriptionColour) + case .suffix(of: let unit): + Text("\(info.upperValue, specifier: self.infoView.touchSpecifier) \(unit)") + .font(.title3) + .foregroundColor(self.chartStyle.infoBoxValueColour) + Text("\(info.pointDescription ?? "")") + .font(.subheadline) + .foregroundColor(self.chartStyle.infoBoxDescriptionColour) + } + } + } + + public typealias Set = RangedBarDataSet + public typealias DataPoint = RangedBarDataPoint + public typealias CTStyle = BarChartStyle +} diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/RangedBarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/RangedBarDataSet.swift new file mode 100644 index 00000000..01204a75 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/RangedBarDataSet.swift @@ -0,0 +1,30 @@ +// +// RangedBarDataSet.swift +// +// +// Created by Will Dale on 05/03/2021. +// + +import SwiftUI + +public struct RangedBarDataSet : CTRangedBarChartDataSet { + + public var id: UUID = UUID() + public var dataPoints : [RangedBarDataPoint] + 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 : [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/RangedBarDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift new file mode 100644 index 00000000..29435835 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift @@ -0,0 +1,46 @@ +// +// RangedBarDataPoint.swift +// +// +// Created by Will Dale on 05/03/2021. +// + +import SwiftUI + +public struct RangedBarDataPoint : CTRangedBarDataPoint { + + public let id = UUID() + + public var upperValue : Double + public var lowerValue : Double + public var xAxisLabel : String? + public var pointDescription : String? + public var date : Date? + public var fillColour : ColourStyle + + + /// Data model for a single data point with colour for use with a 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. + /// - pointLabel: A longer label that can be shown on touch input. + /// - date: Date of the data point if any data based calculations are required. + /// - fillColour: Colour styling for the fill. + public init(lowerValue : Double, + upperValue : Double, + xAxisLabel : String? = nil, + pointLabel : String? = nil, + date : Date? = nil, + fillColour : ColourStyle = ColourStyle(colour: .red) + ) { + self.upperValue = upperValue + self.lowerValue = lowerValue + self.xAxisLabel = xAxisLabel + self.pointDescription = pointLabel + self.date = date + self.fillColour = fillColour + } + + public typealias ID = UUID +} diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index 994b1ba6..47d4eed1 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -33,7 +33,10 @@ public protocol CTMultiBarChartDataProtocol: CTBarChartDataProtocol { var groups : [GroupingData] { get set } } - +/** + A protocol to extend functionality of `CTBarChartDataProtocol` specifically for Multi Part Bar Charts. + */ +public protocol CTRangedBarChartDataProtocol: CTBarChartDataProtocol {} @@ -77,7 +80,7 @@ public protocol CTStandardBarChartDataSet: CTSingleDataSetProtocol { public protocol CTMultiBarChartDataSet: CTSingleDataSetProtocol {} - +public protocol CTRangedBarChartDataSet: CTSingleDataSetProtocol {} @@ -92,7 +95,7 @@ public protocol CTBarDataPoint: CTLineBarDataPointProtocol {} /** A protocol to extend functionality of `CTLineBarDataPointProtocol` specifically for standard Bar Charts. */ -public protocol CTStandardBarDataPoint: CTBarDataPoint { +public protocol CTStandardBarDataPoint: CTBarDataPoint, CTStandardDataPointProtocol, CTnotRanged { /// Drawing style of the range fill. var fillColour : ColourStyle { get set } } @@ -101,7 +104,7 @@ public protocol CTStandardBarDataPoint: CTBarDataPoint { A protocol to extend functionality of `CTLineBarDataPointProtocol` specifically for multi part Bar Charts. i.e: Grouped or Stacked */ -public protocol CTMultiBarDataPoint: CTBarDataPoint { +public protocol CTMultiBarDataPoint: CTBarDataPoint, CTStandardDataPointProtocol, CTnotRanged { /** For grouping data points together so they can be drawn in the correct groupings. @@ -109,3 +112,6 @@ public protocol CTMultiBarDataPoint: CTBarDataPoint { var group : GroupingData { get set } } + +public protocol CTRangedBarDataPoint: CTBarDataPoint, CTRangeDataPointProtocol, CTisRanged {} + diff --git a/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift new file mode 100644 index 00000000..92fbfcc5 --- /dev/null +++ b/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift @@ -0,0 +1,118 @@ +// +// RangedBarChart.swift +// +// +// Created by Will Dale on 05/03/2021. +// + +import SwiftUI + +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 + } + + @State private var startAnimation : Bool = false + + public var body: some View { + + HStack(spacing: 0) { + ForEach(chartData.dataSets.dataPoints) { dataPoint in + GeometryReader { geo in + + if chartData.barStyle.fillColour.colourType == .colour, + let colour = chartData.barStyle.fillColour.colour { + + 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: geo.frame(in: .local).midX, + y: getBarPositionX(dataPoint: dataPoint, height: geo.size.height)) + + .background(Color(.gray).opacity(0.000000001)) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = false + } + .accessibilityValue(Text("\(dataPoint.upperValue, specifier: chartData.infoView.touchSpecifier), \(dataPoint.pointDescription ?? "")")) + + } else if chartData.barStyle.fillColour.colourType == .gradientColour, + let colours = chartData.barStyle.fillColour.colours, + let startPoint = chartData.barStyle.fillColour.startPoint, + let endPoint = chartData.barStyle.fillColour.endPoint { + + 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: geo.frame(in: .local).midX, + y: getBarPositionX(dataPoint: dataPoint, height: geo.size.height)) + + .background(Color(.gray).opacity(0.000000001)) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = false + } + .accessibilityValue(Text("\(dataPoint.upperValue, specifier: chartData.infoView.touchSpecifier), \(dataPoint.pointDescription ?? "")")) + + } else if chartData.barStyle.fillColour.colourType == .gradientStops, + let stops = chartData.barStyle.fillColour.stops, + let startPoint = chartData.barStyle.fillColour.startPoint, + let endPoint = chartData.barStyle.fillColour.endPoint { + + let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) + + 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: safeStops), + 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: geo.frame(in: .local).midX, + y: getBarPositionX(dataPoint: dataPoint, height: geo.size.height)) + + .background(Color(.gray).opacity(0.000000001)) + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = true + } + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { + self.startAnimation = false + } + .accessibilityValue(Text("\(dataPoint.upperValue, specifier: chartData.infoView.touchSpecifier), \(dataPoint.pointDescription ?? "")")) + + } + + } + } + } + } + + private func getBarPositionX(dataPoint: RangedBarDataPoint, height: CGFloat) -> CGFloat { + let value = CGFloat((dataPoint.upperValue + dataPoint.lowerValue) / 2) - CGFloat(chartData.minValue) + return (height - (value / CGFloat(chartData.range)) * height) + } +} From 32be6ff10ba12c0fc836e7e237ddc10d82ff1673 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 5 Mar 2021 09:30:27 +0000 Subject: [PATCH 118/152] Rename Protocols. --- .../BarChart/Views/SubViews/Bars.swift | 6 +- .../LineChart/Extras/PathExtensions.swift | 4 +- .../DataPoints/LineChartDataPoint.swift | 2 +- .../Models/Protocols/LineChartProtocols.swift | 138 ++---------------- .../LineChartProtocolsExtensions.swift | 128 +++++++++++++++- .../LineChart/Shapes/LineShape.swift | 2 +- .../LineChart/Shapes/PointShape.swift | 3 +- .../Views/SubViews/LineChartSubViews.swift | 6 +- .../Views/SubViews/PointsSubView.swift | 3 +- .../Models/Protocols/PieChartProtocols.swift | 4 +- .../Shared/Models/InfoViewData.swift | 2 +- .../Protocols/LineAndBarProtocols.swift | 6 +- 12 files changed, 155 insertions(+), 149 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift index 9ab870f3..c170c3cd 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift @@ -13,7 +13,7 @@ import SwiftUI For Standard and Grouped Bar Charts. */ -internal struct ColourBar: View { +internal struct ColourBar: View { private let colour : Color private let data : DP @@ -68,7 +68,7 @@ internal struct ColourBar: View { For Standard and Grouped Bar Charts. */ -internal struct GradientColoursBar: View { +internal struct GradientColoursBar: View { private let colours : [Color] private let startPoint : UnitPoint @@ -131,7 +131,7 @@ internal struct GradientColoursBar: View { For Standard and Grouped Bar Charts. */ -internal struct GradientStopsBar: View { +internal struct GradientStopsBar: View { private let stops : [Gradient.Stop] private let startPoint : UnitPoint diff --git a/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift b/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift index 5ed13d05..b075fac3 100644 --- a/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift @@ -10,7 +10,7 @@ import SwiftUI // MARK: - Paths extension Path { /// Draws straight lines between data points. - static func straightLine(rect: CGRect, dataPoints: [DP], minValue: Double, range: Double, isFilled: Bool) -> Path { + static func straightLine(rect: CGRect, dataPoints: [DP], minValue: Double, range: Double, isFilled: Bool) -> Path { let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1) let y : CGFloat = rect.height / CGFloat(range) var path = Path() @@ -31,7 +31,7 @@ extension Path { } /// Draws cubic BĆ©zier curved lines between data points. - static func curvedLine(rect: CGRect, dataPoints: [DP], minValue: Double, range: Double, isFilled: Bool) -> Path { + static func curvedLine(rect: CGRect, dataPoints: [DP], minValue: Double, range: Double, isFilled: Bool) -> Path { let x : CGFloat = rect.width / CGFloat(dataPoints.count - 1) let y : CGFloat = rect.height / CGFloat(range) var path = Path() diff --git a/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift b/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift index a673352e..12aa8d08 100644 --- a/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift @@ -18,7 +18,7 @@ import SwiftUI date : Date()) ``` */ -public struct LineChartDataPoint: CTLineDataPointProtocol { +public struct LineChartDataPoint: CTStandardLineDataPoint { public let id : UUID = UUID() diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index e236f83c..d8da1c58 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -33,126 +33,8 @@ public protocol CTLineChartDataProtocol: CTLineBarChartDataProtocol { func getAccessibility() -> Access } -extension CTLineChartDataProtocol where Self.Set.ID == UUID, - Self.Set: CTLineChartDataSet { - internal func setupLegends() { - - if dataSets.style.lineColour.colourType == .colour, - let colour = dataSets.style.lineColour.colour - { - self.legends.append(LegendData(id : dataSets.id, - legend : dataSets.legendTitle, - colour : ColourStyle(colour: colour), - strokeStyle: dataSets.style.strokeStyle, - prioity : 1, - chartType : .line)) - - } else if dataSets.style.lineColour.colourType == .gradientColour, - let colours = dataSets.style.lineColour.colours - { - self.legends.append(LegendData(id : dataSets.id, - legend : dataSets.legendTitle, - colour : ColourStyle(colours: colours, - startPoint: .leading, - endPoint: .trailing), - strokeStyle: dataSets.style.strokeStyle, - prioity : 1, - chartType : .line)) - - } else if dataSets.style.lineColour.colourType == .gradientStops, - let stops = dataSets.style.lineColour.stops - { - self.legends.append(LegendData(id : dataSets.id, - legend : dataSets.legendTitle, - colour : ColourStyle(stops: stops, - startPoint: .leading, - endPoint: .trailing), - strokeStyle: dataSets.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 : .line)) - - } 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 : .line)) - } - } -} -extension CTLineChartDataProtocol where Self.Set == MultiLineDataSet { - internal func setupLegends() { - for dataSet in dataSets.dataSets { - 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)) - } - } - } -} + + // MARK: - Style /** @@ -227,12 +109,12 @@ public protocol CTMultiLineChartDataSet: CTMultiDataSetProtocol {} public protocol CTLineDataPointProtocol: CTLineBarDataPointProtocol {} /** - A protocol to extend functionality of `CTChartDataPointProtocol` specifically for Ranged Line Charts. + A protocol to extend functionality of `CTStandardDataPointProtocol` specifically for Ranged Line Charts. */ -public protocol CTRangedLineDataPoint: CTLineDataPointProtocol { - /// 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 } -} +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 index b2d3baaf..346513a2 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -10,7 +10,7 @@ import SwiftUI // MARK: - Position Indicator extension CTLineChartDataProtocol { - public static func getIndicatorLocation(rect: CGRect, + public static func getIndicatorLocation(rect: CGRect, dataPoints: [DP], touchLocation: CGPoint, lineType: LineType, @@ -42,7 +42,7 @@ extension CTLineChartDataProtocol { - 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, touchLocation: CGPoint, isFilled: Bool) -> Path { + static func getPath(lineType: LineType, rect: CGRect, dataPoints: [DP], minValue: Double, range: Double, touchLocation: CGPoint, isFilled: Bool) -> Path { switch lineType { case .line: return Path.straightLine(rect : rect, @@ -245,7 +245,7 @@ extension CTLineChartDataProtocol { extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { internal func markerSubView + DP: CTStandardDataPointProtocol> (dataSet : DS, dataPoints : [DP], lineType : LineType, @@ -462,3 +462,125 @@ internal struct IndicatorSwitch: View { } } + +// MARK: - Legends +extension CTLineChartDataProtocol where Self.Set.ID == UUID, + Self.Set: CTLineChartDataSet { + internal func setupLegends() { + + if dataSets.style.lineColour.colourType == .colour, + let colour = dataSets.style.lineColour.colour + { + self.legends.append(LegendData(id : dataSets.id, + legend : dataSets.legendTitle, + colour : ColourStyle(colour: colour), + strokeStyle: dataSets.style.strokeStyle, + prioity : 1, + chartType : .line)) + + } else if dataSets.style.lineColour.colourType == .gradientColour, + let colours = dataSets.style.lineColour.colours + { + self.legends.append(LegendData(id : dataSets.id, + legend : dataSets.legendTitle, + colour : ColourStyle(colours: colours, + startPoint: .leading, + endPoint: .trailing), + strokeStyle: dataSets.style.strokeStyle, + prioity : 1, + chartType : .line)) + + } else if dataSets.style.lineColour.colourType == .gradientStops, + let stops = dataSets.style.lineColour.stops + { + self.legends.append(LegendData(id : dataSets.id, + legend : dataSets.legendTitle, + colour : ColourStyle(stops: stops, + startPoint: .leading, + endPoint: .trailing), + strokeStyle: dataSets.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 : .line)) + + } 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 : .line)) + } + } +} +extension CTLineChartDataProtocol where Self.Set == MultiLineDataSet { + internal func setupLegends() { + for dataSet in dataSets.dataSets { + 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)) + } + } + } +} diff --git a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift index c9071cf9..695dc4e4 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift @@ -10,7 +10,7 @@ import SwiftUI /** Main line shape */ -internal struct LineShape: Shape where DP: CTLineDataPointProtocol { +internal struct LineShape: Shape where DP: CTStandardDataPointProtocol { private let dataPoints : [DP] private let lineType : LineType diff --git a/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift index a72b0043..850a7859 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift @@ -10,7 +10,8 @@ import SwiftUI /** Draws point markers over the data point locations. */ -internal struct Point: Shape where T: CTLineChartDataSet { +internal struct Point: Shape where T: CTLineChartDataSet, + T.DataPoint: CTStandardDataPointProtocol { private let dataSet : T diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift index 80fd4f11..5d3fbe45 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift @@ -37,7 +37,7 @@ struct AccessibilityRectangle: Shape { */ internal struct LineChartColourSubView: View where CD: CTLineChartDataProtocol, DS: CTLineChartDataSet, - DS.DataPoint: CTLineDataPointProtocol { + DS.DataPoint: CTStandardDataPointProtocol { private let chartData : CD private let dataSet : DS @@ -98,7 +98,7 @@ internal struct LineChartColourSubView: View where CD: CTLineChartDataPr */ internal struct LineChartColoursSubView: View where CD: CTLineChartDataProtocol, DS: CTLineChartDataSet, - DS.DataPoint: CTLineDataPointProtocol { + DS.DataPoint: CTStandardDataPointProtocol { private let chartData : CD private let dataSet : DS @@ -180,7 +180,7 @@ internal struct LineChartColoursSubView: View where CD: CTLineChartDataP */ internal struct LineChartStopsSubView: View where CD: CTLineChartDataProtocol, DS: CTLineChartDataSet, - DS.DataPoint: CTLineDataPointProtocol { + DS.DataPoint: CTStandardDataPointProtocol { private let chartData : CD private let dataSet : DS diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift index 2416a0f5..af4738b2 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/PointsSubView.swift @@ -10,7 +10,8 @@ import SwiftUI /** Sub view gets the point markers drawn, sets the styling and sets up the animations. */ -internal struct PointsSubView: View where DS: CTLineChartDataSet { +internal struct PointsSubView: View where DS: CTLineChartDataSet, + DS.DataPoint: CTStandardDataPointProtocol { private let dataSets : DS private let minValue : Double diff --git a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift index 84fc2242..8059b456 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocols.swift @@ -26,9 +26,9 @@ public protocol CTDoughnutChartDataProtocol : CTPieDoughnutChartDataProtocol {} // MARK: - DataPoints /** - A protocol to extend functionality of `CTChartDataPointProtocol` specifically for Pie and Doughnut Charts. + A protocol to extend functionality of `CTStandardDataPointProtocol` specifically for Pie and Doughnut Charts. */ -public protocol CTPieDataPoint: CTChartDataPointProtocol { +public protocol CTPieDataPoint: CTStandardDataPointProtocol, CTnotRanged { /** Where the data point should start drawing from diff --git a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift index 802e82b3..939af996 100644 --- a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift @@ -10,7 +10,7 @@ import SwiftUI /** Data model to pass view information internally for the `InfoBox` and `HeaderBox`. */ -public struct InfoViewData { +public struct InfoViewData { /** Is there currently input (touch or click) on the chart. diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index 349d9323..a42d10b0 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -11,7 +11,7 @@ import SwiftUI /** A protocol to extend functionality of `CTChartData` specifically for Line and Bar Charts. */ -public protocol CTLineBarChartDataProtocol : CTChartData where CTStyle: CTLineBarChartStyle { +public protocol CTLineBarChartDataProtocol: CTChartData where CTStyle: CTLineBarChartStyle { /// A type representing opaque View associatedtype XLabels : View @@ -142,9 +142,9 @@ public protocol CTLineBarChartStyle: CTChartStyle { // MARK: - DataPoints /** - A protocol to extend functionality of `CTChartDataPointProtocol` specifically for Line and Bar Charts. + A protocol to extend functionality of `CTStandardDataPointProtocol` specifically for Line and Bar Charts. */ -public protocol CTLineBarDataPointProtocol: CTChartDataPointProtocol { +public protocol CTLineBarDataPointProtocol: CTDataPointBaseProtocol { /** Data points label for the X axis. From df3d04ce82974391659520122918360c294af3b6 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 5 Mar 2021 09:31:15 +0000 Subject: [PATCH 119/152] Restructure data functions. --- .../ChartData/RangedLineChartData.swift | 65 +++++---- .../Shared/Extras/DataFunctions.swift | 127 ------------------ .../Protocols/SharedProtocolsExtensions.swift | 75 +++++++++++ 3 files changed, 106 insertions(+), 161 deletions(-) delete mode 100644 Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift index b409220b..d1073eb3 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift @@ -52,43 +52,12 @@ public final class RangedLineChartData: CTLineChartDataProtocol { self.setupLegends() self.setupRangeLegends() } - // MARK: Data - public var range : Double { - - var _lowestValue : Double - var _highestValue : Double - - switch self.chartStyle.baseline { - case .minimumValue: - _lowestValue = dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0 - case .minimumWithMaximum(of: let value): - _lowestValue = min(dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0, value) - case .zero: - _lowestValue = 0 - } - - switch self.chartStyle.topLine { - case .maximumValue: - _highestValue = dataSets.dataPoints.max(by: { $0.upperValue < $1.upperValue })?.upperValue ?? 0 - case .maximum(of: let value): - _highestValue = max(dataSets.dataPoints.max(by: { $0.upperValue < $1.upperValue })?.upperValue ?? 0, value) - } - - return (_highestValue - _lowestValue) + 0.001 - } - public var minValue : Double { - switch self.chartStyle.baseline { - case .minimumValue: - return dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0 - case .minimumWithMaximum(of: let value): - return min(dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0, value) - case .zero: - return 0 - } + 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 { @@ -180,6 +149,34 @@ public final class RangedLineChartData: CTLineChartDataProtocol { } self.infoView.touchOverlayInfo = points } + + public func headerTouchOverlaySubView(info: RangedLineChartDataPoint) -> some View { + Group { + switch self.infoView.touchUnit { + case .none: + Text("\(info.upperValue, specifier: self.infoView.touchSpecifier)") + .font(.title3) + .foregroundColor(self.chartStyle.infoBoxValueColour) + Text("\(info.pointDescription ?? "")") + .font(.subheadline) + .foregroundColor(self.chartStyle.infoBoxDescriptionColour) + case .prefix(of: let unit): + Text("\(unit) \(info.upperValue, specifier: self.infoView.touchSpecifier)") + .font(.title3) + .foregroundColor(self.chartStyle.infoBoxValueColour) + Text("\(info.pointDescription ?? "")") + .font(.subheadline) + .foregroundColor(self.chartStyle.infoBoxDescriptionColour) + case .suffix(of: let unit): + Text("\(info.upperValue, specifier: self.infoView.touchSpecifier) \(unit)") + .font(.title3) + .foregroundColor(self.chartStyle.infoBoxValueColour) + Text("\(info.pointDescription ?? "")") + .font(.subheadline) + .foregroundColor(self.chartStyle.infoBoxDescriptionColour) + } + } + } // MARK: Accessibility public func getAccessibility() -> some View { diff --git a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift b/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift deleted file mode 100644 index 6e5c58e2..00000000 --- a/Sources/SwiftUICharts/Shared/Extras/DataFunctions.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// DataFunctions.swift -// -// -// Created by Will Dale on 23/01/2021. -// - -import Foundation - -/** - A collection of functions for getting infomation about the data sets. -*/ - struct DataFunctions { - - // MARK: - Single Data Set - /** - Returns the highest value in the data set. - - Parameter dataSet: Target data set. - - Returns: Highest value in data set. - */ - static func dataSetMaxValue(from dataSet: T) -> Double { - return dataSet.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. - */ - static func dataSetMinValue(from dataSet: T) -> Double { - return dataSet.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. - */ - static func dataSetAverage(from dataSet: T) -> Double { - let sum = dataSet.dataPoints.reduce(0) { $0 + $1.value } - return sum / Double(dataSet.dataPoints.count) - } - - /** - Returns the difference between the highest and lowest numbers in the data set. - - Parameter dataSet: Target data set. - - Returns: Difference between the highest and lowest values in data set. - */ - static func dataSetRange(from dataSet: T) -> Double { - let maxValue = dataSet.dataPoints.max { $0.value < $1.value }?.value ?? 0 - let minValue = dataSet.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 - } - - - // MARK: - Multi Data Sets - /** - Returns the highest value in the data sets - - Parameter dataSet: Target data sets. - - Returns: Highest value in data sets. - */ - static func multiDataSetMaxValue(from dataSets: T) -> Double { - var setHolder : [Double] = [] - for set in dataSets.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. - */ - static func multiDataSetMinValue(from dataSets: T) -> Double { - var setHolder : [Double] = [] - for set in dataSets.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. - */ - static func multiDataSetAverage(from dataSets: T) -> Double { - var setHolder : [Double] = [] - for set in dataSets.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) - } - - /** - Returns the difference between the highest and lowest numbers in the data sets. - - Parameter dataSet: Target data sets. - - Returns: Difference between the highest and lowest values in data sets. - */ - static func multiDataSetRange(from dataSets: T) -> Double { - var setMaxHolder : [Double] = [] - for set in dataSets.dataSets { - setMaxHolder.append(set.dataPoints.max { $0.value < $1.value }?.value ?? 0) - } - let maxValue = setMaxHolder.max { $0 < $1 } ?? 0 - - var setMinHolder : [Double] = [] - for set in dataSets.dataSets { - setMinHolder.append(set.dataPoints.min { $0.value < $1.value }?.value ?? 0) - } - let minValue = setMinHolder.min { $0 < $1 } ?? 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 - } -} diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift index b04a924c..dfc94328 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift @@ -36,3 +36,78 @@ extension CTChartData { return legends.sorted { $0.prioity < $1.prioity} } } + +// MARK: - Data Set +extension CTSingleDataSetProtocol where Self.DataPoint: CTStandardDataPointProtocol { + /** + Returns the highest value in the data set. + - Parameter dataSet: Target data set. + - Returns: Highest value in data set. + */ + 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. + */ + 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. + */ + func average() -> Double { + let sum = self.dataPoints.reduce(0) { $0 + $1.value } + 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. + */ + 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. + */ + 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. + */ + 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) + } +} From ca6f04663b23edcb8f7e45d6a332dcd14d110cae Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 5 Mar 2021 09:32:30 +0000 Subject: [PATCH 120/152] Move Touch info view into protocol. --- .../Models/Protocols/SharedProtocols.swift | 95 ++++++++++++++++--- .../Shared/ViewModifiers/HeaderBox.swift | 26 +---- .../Shared/ViewModifiers/InfoBox.swift | 18 +--- .../Shared/Views/TouchOverlayBox.swift | 66 ++----------- 4 files changed, 95 insertions(+), 110 deletions(-) diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 4afd53c6..5bb9da11 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -17,19 +17,20 @@ import SwiftUI public protocol CTChartData: ObservableObject, Identifiable { /// A type representing a data set. -- `CTDataSetProtocol` - associatedtype Set : CTDataSetProtocol + associatedtype Set: CTDataSetProtocol + + /// A type representing a data set. -- `CTDataSetProtocol` + associatedtype SetPoint: CTDataSetProtocol /// A type representing a data point. -- `CTChartDataPoint` - associatedtype DataPoint: CTChartDataPointProtocol + associatedtype DataPoint: CTDataPointBaseProtocol /// A type representing the chart style. -- `CTChartStyle` - associatedtype CTStyle : CTChartStyle + associatedtype CTStyle: CTChartStyle /// A type representing opaque View - associatedtype Touch : View + associatedtype Touch: View - /// A type representing a data set. -- `CTDataSetProtocol` - associatedtype SetPoint : CTDataSetProtocol var id: ID { get } @@ -121,8 +122,45 @@ public protocol CTChartData: ObservableObject, Identifiable { - Returns: Array of points with the location on screen of data points. */ func getPointLocation(dataSet: SetPoint, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? + + associatedtype TouchInformation: View + + func headerTouchOverlaySubView(info: DataPoint) -> TouchInformation } +extension CTChartData where Self.DataPoint : CTStandardDataPointProtocol { + public func headerTouchOverlaySubView(info: Self.DataPoint) -> some View { + Group { + switch self.infoView.touchUnit { + case .none: + Text("\(info.value, specifier: self.infoView.touchSpecifier)") + .font(.title3) + .foregroundColor(self.chartStyle.infoBoxValueColour) + Text("\(info.pointDescription ?? "")") + .font(.subheadline) + .foregroundColor(self.chartStyle.infoBoxDescriptionColour) + case .prefix(of: let unit): + Text("\(unit) \(info.value, specifier: self.infoView.touchSpecifier)") + .font(.title3) + .foregroundColor(self.chartStyle.infoBoxValueColour) + Text("\(info.pointDescription ?? "")") + .font(.subheadline) + .foregroundColor(self.chartStyle.infoBoxDescriptionColour) + case .suffix(of: let unit): + Text("\(info.value, specifier: self.infoView.touchSpecifier) \(unit)") + .font(.title3) + .foregroundColor(self.chartStyle.infoBoxValueColour) + Text("\(info.pointDescription ?? "")") + .font(.subheadline) + .foregroundColor(self.chartStyle.infoBoxDescriptionColour) + } + } + } +} + + + + // MARK: - Data Sets /** Main protocol to set conformace for types of Data Sets. @@ -136,7 +174,7 @@ public protocol CTDataSetProtocol: Hashable, Identifiable { */ public protocol CTSingleDataSetProtocol: CTDataSetProtocol { /// A type representing a data point. -- `CTChartDataPoint` - associatedtype DataPoint : CTChartDataPointProtocol + associatedtype DataPoint : CTDataPointBaseProtocol /** Array of data points. @@ -158,23 +196,21 @@ public protocol CTMultiDataSetProtocol: CTDataSetProtocol { var dataSets : [DataSet] { get set } } + + + + // MARK: - Data Points /** Protocol to set base configuration for data points. */ -public protocol CTChartDataPointProtocol: Hashable, Identifiable { - +public protocol CTDataPointBaseProtocol: Hashable, Identifiable { var id : ID { get } - /** - Value of the data point - */ - var value : Double { get set } - /** A label that can be displayed on touch input - It can eight be displayed in a floating box that tracks the users input location + It can be displayed in a floating box that tracks the users input location or placed in the header. */ var pointDescription : String? { get set } @@ -183,9 +219,35 @@ public protocol CTChartDataPointProtocol: Hashable, Identifiable { Date can be used for optionally performing additional calculations. */ var date : Date? { get set } +} + +/** + 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. @@ -249,3 +311,6 @@ public protocol CTColourStyle { /// End point for the gradient var endPoint: UnitPoint? { get set } } + +public protocol CTisRanged {} +public protocol CTnotRanged {} diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index 6b069e63..a058e25a 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -29,35 +29,13 @@ internal struct HeaderBox: ViewModifier where T: CTChartData { .foregroundColor(chartData.metadata.subtitleColour) } } - var touchOverlay: some View { + VStack(alignment: .trailing) { if chartData.infoView.isTouchCurrent { ForEach(chartData.infoView.touchOverlayInfo, id: \.self) { info in - switch chartData.infoView.touchUnit { - case .none: - Text("\(info.value, specifier: chartData.infoView.touchSpecifier)") - .font(.title3) - .foregroundColor(chartData.chartStyle.infoBoxValueColour) - Text("\(info.pointDescription ?? "")") - .font(.subheadline) - .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) - case .prefix(of: let unit): - Text("\(unit) \(info.value, specifier: chartData.infoView.touchSpecifier)") - .font(.title3) - .foregroundColor(chartData.chartStyle.infoBoxValueColour) - Text("\(info.pointDescription ?? "")") - .font(.subheadline) - .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) - case .suffix(of: let unit): - Text("\(info.value, specifier: chartData.infoView.touchSpecifier) \(unit)") - .font(.title3) - .foregroundColor(chartData.chartStyle.infoBoxValueColour) - Text("\(info.pointDescription ?? "")") - .font(.subheadline) - .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) - } + chartData.headerTouchOverlaySubView(info: info) } } else { Text("") diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift index 24ddb6dd..ead3c232 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift @@ -31,13 +31,8 @@ internal struct InfoBox: ViewModifier where T: CTChartData { } var floating: some View { - TouchOverlayBox(isTouchCurrent : chartData.infoView.isTouchCurrent, - selectedPoints : chartData.infoView.touchOverlayInfo, - specifier : chartData.infoView.touchSpecifier, - unit : chartData.infoView.touchUnit, - valueColour : chartData.chartStyle.infoBoxValueColour, - descriptionColour: chartData.chartStyle.infoBoxDescriptionColour, - boxFrame : $boxFrame) + TouchOverlayBox(chartData: chartData, + boxFrame : $boxFrame) .position(x: setBoxLocationation(touchLocation: chartData.infoView.touchLocation.x, boxFrame : boxFrame, chartSize : chartData.infoView.chartSize), @@ -48,13 +43,8 @@ internal struct InfoBox: ViewModifier where T: CTChartData { var fixed: some View { - TouchOverlayBox(isTouchCurrent : chartData.infoView.isTouchCurrent, - selectedPoints : chartData.infoView.touchOverlayInfo, - specifier : chartData.infoView.touchSpecifier, - unit : chartData.infoView.touchUnit, - valueColour : chartData.chartStyle.infoBoxValueColour, - descriptionColour: chartData.chartStyle.infoBoxDescriptionColour, - boxFrame : $boxFrame) + TouchOverlayBox(chartData: chartData, + boxFrame : $boxFrame) .frame(height: 40) .padding(.horizontal, 6) diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index e7fd6759..0257247e 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -10,81 +10,33 @@ import SwiftUI /** View that displays information from the touch events. */ -internal struct TouchOverlayBox: View { +internal struct TouchOverlayBox: View { - private var isTouchCurrent : Bool - private var selectedPoints : [D] - private var specifier : String - private var unit : Unit - - private var valueColour : Color - private var descriptionColour : Color - - private var ignoreZero : Bool + @ObservedObject var chartData: T @Binding private var boxFrame : CGRect - internal init(isTouchCurrent : Bool, - selectedPoints : [D], - specifier : String = "%.0f", - unit : Unit, - valueColour : Color, - descriptionColour : Color, - boxFrame : Binding, - ignoreZero : Bool = false + internal init(chartData : T, + boxFrame : Binding ) { - self.isTouchCurrent = isTouchCurrent - self.selectedPoints = selectedPoints - self.specifier = specifier - self.unit = unit - self.valueColour = valueColour - self.descriptionColour = descriptionColour + self.chartData = chartData self._boxFrame = boxFrame - self.ignoreZero = ignoreZero } internal var body: some View { HStack { - ForEach(selectedPoints, id: \.self) { point in + ForEach(chartData.infoView.touchOverlayInfo, id: \.self) { point in - switch unit { - case .none: - Text("\(point.value, specifier: specifier)") - .font(.subheadline) - .foregroundColor(valueColour) - if let label = point.pointDescription { - Text(label) - .font(.subheadline) - .foregroundColor(descriptionColour) - } - case .prefix(of: let unit): - Text("\(unit) \(point.value, specifier: specifier)") - .font(.subheadline) - .foregroundColor(valueColour) - if let label = point.pointDescription { - Text(label) - .font(.subheadline) - .foregroundColor(descriptionColour) - } - case .suffix(of: let unit): - Text("\(point.value, specifier: specifier) \(unit)") - .font(.subheadline) - .foregroundColor(valueColour) - if let label = point.pointDescription { - Text(label) - .font(.subheadline) - .foregroundColor(descriptionColour) - } - } - + chartData.headerTouchOverlaySubView(info: point) + } } .padding(.all, 8) .background( GeometryReader { geo in - if isTouchCurrent { + if chartData.infoView.isTouchCurrent { Group { RoundedRectangle(cornerRadius: 5.0, style: .continuous) .fill(Color.systemsBackground) From 6de41b63f6f77b1104d3e54fcb2117a3ba17d7f7 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 5 Mar 2021 09:32:53 +0000 Subject: [PATCH 121/152] Fix marker position. --- .../SharedLineAndBar/Shapes/Marker.swift | 4 +-- .../ViewModifiers/YAxisPOI.swift | 32 +++++++++++-------- .../Views/ValueLabelCenterSubView.swift | 15 --------- .../Views/ValueLabelYAxisSubView.swift | 20 ------------ 4 files changed, 21 insertions(+), 50 deletions(-) diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift index 69aad5bd..4f33503a 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift @@ -40,8 +40,8 @@ internal struct Marker: Shape { let y = rect.height / CGFloat(range) pointY = (CGFloat(value - minValue) * -y) + rect.height case .bar: - let y = rect.height / CGFloat(maxValue) - pointY = rect.height - CGFloat(value) * y + let y = CGFloat(value - minValue) + pointY = (rect.height - (y / CGFloat(range)) * rect.height) case .pie: pointY = 0 } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index af8df0b4..db1e8400 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -110,6 +110,9 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { labelBackground: labelBackground, lineColour : lineColour, chartSize : geo.frame(in: .local)) + .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)")) @@ -123,24 +126,27 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { lineColour : lineColour, strokeStyle : strokeStyle, chartSize : geo.frame(in: .local)) + .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, chartSize: CGRect) -> CGFloat { - switch chartData.chartType.chartType { - case .line: - let y = chartSize.height / CGFloat(range) - return (CGFloat(markerValue - minValue) * -y) + chartSize.size.height - case .bar: - let y = chartSize.height / CGFloat(maxValue) - return chartSize.height - CGFloat(markerValue) * y - case .pie: - return 0 - } - } + 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 + } + } } extension View { diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelCenterSubView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelCenterSubView.swift index 64d7e0c0..5621c3b1 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelCenterSubView.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelCenterSubView.swift @@ -49,8 +49,6 @@ internal struct ValueLabelCenterSubView: View where T: CTLineBarChartDataProt .overlay(DiamondShape() .stroke(lineColour, style: strokeStyle) ) - .position(x: chartSize.width / 2, - y: getYPoint(chartType: chartData.chartType.chartType, chartSize: chartSize)) .opacity(startAnimation ? 1 : 0) .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true @@ -60,18 +58,5 @@ internal struct ValueLabelCenterSubView: View where T: CTLineBarChartDataProt } } - - private func getYPoint(chartType: ChartType, chartSize: CGRect) -> CGFloat { - switch chartData.chartType.chartType { - case .line: - let y = chartSize.height / CGFloat(chartData.range) - return (CGFloat(markerValue - chartData.minValue) * -y) + chartSize.height - case .bar: - let y = chartSize.height / CGFloat(chartData.maxValue) - return chartSize.height - CGFloat(markerValue) * y - case .pie: - return 0 - } - } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelYAxisSubView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelYAxisSubView.swift index 85cc230a..7fe6f578 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelYAxisSubView.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelYAxisSubView.swift @@ -40,38 +40,18 @@ internal struct ValueLabelYAxisSubView: View where T: CTLineBarChartDataProto .foregroundColor(labelColour) .padding(4) .background(labelBackground) - .ifElse(self.chartData.chartStyle.yAxisLabelPosition == .leading, if: { $0 .clipShape(LeadingLabelShape()) .overlay(LeadingLabelShape() .stroke(lineColour) ) - - .position(x: -(chartData.infoView.yAxisLabelWidth / 2) - 6, - y: getYPoint(chartType: chartData.chartType.chartType, chartSize: chartSize)) }, else: { $0 .clipShape(TrailingLabelShape()) .overlay(TrailingLabelShape() .stroke(lineColour) ) - .position(x: chartSize.width + (chartData.infoView.yAxisLabelWidth / 2), - y: getYPoint(chartType: chartData.chartType.chartType, chartSize: chartSize)) }) - } - - private func getYPoint(chartType: ChartType, chartSize: CGRect) -> CGFloat { - switch chartData.chartType.chartType { - case .line: - let y = chartSize.height / CGFloat(chartData.range) - return (CGFloat(markerValue - chartData.minValue) * -y) + chartSize.height - case .bar: - let y = chartSize.height / CGFloat(chartData.maxValue) - return chartSize.height - CGFloat(markerValue) * y - case .pie: - return 0 - } - } } From 0d4c9c2835a9b8d3d3eaf571ceccbffdd5e32e7e Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 5 Mar 2021 09:33:20 +0000 Subject: [PATCH 122/152] Restructure data functions. --- .../BarChart/Views/StackedBarChart.swift | 2 +- .../LineAndBarProtocolsExtentions.swift | 91 ++++++++++++++----- 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift index 66294908..126cb208 100644 --- a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift @@ -55,7 +55,7 @@ public struct StackedBarChart: View where ChartData: StackedBarChartD ForEach(chartData.dataSets.dataSets) { dataSet in StackElementSubView(dataSet: dataSet) - .scaleEffect(y: startAnimation ? CGFloat(DataFunctions.dataSetMaxValue(from: dataSet) / chartData.maxValue) : 0, anchor: .bottom) + .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) { diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift index 98c1e70f..76784852 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift @@ -8,7 +8,8 @@ import Foundation // MARK: - Single Data Set -extension CTLineBarChartDataProtocol where Set: CTSingleDataSetProtocol { +extension CTLineBarChartDataProtocol where Set: CTSingleDataSetProtocol, + Set.DataPoint: CTStandardDataPointProtocol & CTnotRanged { public var range : Double { var _lowestValue : Double @@ -16,18 +17,18 @@ extension CTLineBarChartDataProtocol where Set: CTSingleDataSetProtocol { switch self.chartStyle.baseline { case .minimumValue: - _lowestValue = DataFunctions.dataSetMinValue(from: dataSets) + _lowestValue = self.dataSets.minValue() case .minimumWithMaximum(of: let value): - _lowestValue = min(DataFunctions.dataSetMinValue(from: dataSets), value) + _lowestValue = min(self.dataSets.minValue(), value) case .zero: _lowestValue = 0 } switch self.chartStyle.topLine { case .maximumValue: - _highestValue = DataFunctions.dataSetMaxValue(from: dataSets) + _highestValue = self.dataSets.maxValue() case .maximum(of: let value): - _highestValue = max(DataFunctions.dataSetMaxValue(from: dataSets), value) + _highestValue = max(self.dataSets.maxValue(), value) } return (_highestValue - _lowestValue) + 0.001 @@ -36,9 +37,9 @@ extension CTLineBarChartDataProtocol where Set: CTSingleDataSetProtocol { public var minValue : Double { switch self.chartStyle.baseline { case .minimumValue: - return DataFunctions.dataSetMinValue(from: dataSets) + return self.dataSets.minValue() case .minimumWithMaximum(of: let value): - return min(DataFunctions.dataSetMinValue(from: dataSets), value) + return min(self.dataSets.minValue(), value) case .zero: return 0 } @@ -47,20 +48,66 @@ extension CTLineBarChartDataProtocol where Set: CTSingleDataSetProtocol { public var maxValue : Double { switch self.chartStyle.topLine { case .maximumValue: - return DataFunctions.dataSetMaxValue(from: dataSets) + return self.dataSets.maxValue() case .maximum(of: let value): - return max(DataFunctions.dataSetMaxValue(from: dataSets), value) + return max(self.dataSets.maxValue(), value) } } public var average : Double { - return DataFunctions.dataSetAverage(from: dataSets) + return self.dataSets.average() + } +} +extension CTLineBarChartDataProtocol where Set: CTSingleDataSetProtocol, + Set.DataPoint: CTRangeDataPointProtocol & CTisRanged { + public var range : Double { + + var _lowestValue : Double + var _highestValue : Double + + switch self.chartStyle.baseline { + case .minimumValue: + _lowestValue = dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0 + case .minimumWithMaximum(of: let value): + _lowestValue = min(dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0, value) + case .zero: + _lowestValue = 0 + } + + switch self.chartStyle.topLine { + case .maximumValue: + _highestValue = dataSets.dataPoints.max(by: { $0.upperValue < $1.upperValue })?.upperValue ?? 0 + case .maximum(of: let value): + _highestValue = max(dataSets.dataPoints.max(by: { $0.upperValue < $1.upperValue })?.upperValue ?? 0, value) + } + + return (_highestValue - _lowestValue) + 0.001 + } + + public var minValue : Double { + switch self.chartStyle.baseline { + case .minimumValue: + return dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0 + case .minimumWithMaximum(of: let value): + return min(dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0, value) + case .zero: + return 0 + } + } + + public var maxValue : Double { + switch self.chartStyle.topLine { + case .maximumValue: + return dataSets.dataPoints.max(by: { $0.upperValue < $1.upperValue })?.upperValue ?? 0 + case .maximum(of: let value): + return max(dataSets.dataPoints.max(by: { $0.upperValue < $1.upperValue })?.upperValue ?? 0, value) + } } } - // MARK: - Multi Data Set -extension CTLineBarChartDataProtocol where Set: CTMultiDataSetProtocol { +extension CTLineBarChartDataProtocol where Set: CTMultiDataSetProtocol, + Self.Set.DataSet.DataPoint: CTStandardDataPointProtocol { public var range : Double { var _lowestValue : Double @@ -68,28 +115,29 @@ extension CTLineBarChartDataProtocol where Set: CTMultiDataSetProtocol { switch self.chartStyle.baseline { case .minimumValue: - _lowestValue = DataFunctions.multiDataSetMinValue(from: dataSets) + _lowestValue = self.dataSets.minValue() case .minimumWithMaximum(of: let value): - _lowestValue = min(DataFunctions.multiDataSetMinValue(from: dataSets), value) + _lowestValue = min(self.dataSets.minValue(), value) case .zero: _lowestValue = 0 } switch self.chartStyle.topLine { case .maximumValue: - _highestValue = DataFunctions.multiDataSetMaxValue(from: dataSets) + _highestValue = self.dataSets.maxValue() case .maximum(of: let value): - _highestValue = max(DataFunctions.multiDataSetMaxValue(from: dataSets), value) + _highestValue = max(self.dataSets.maxValue(), value) } return (_highestValue - _lowestValue) + 0.001 } + public var minValue : Double { switch self.chartStyle.baseline { case .minimumValue: - return DataFunctions.multiDataSetMinValue(from: dataSets) + return self.dataSets.minValue() case .minimumWithMaximum(of: let value): - return min(DataFunctions.multiDataSetMinValue(from: dataSets), value) + return min(self.dataSets.minValue(), value) case .zero: return 0 } @@ -98,19 +146,18 @@ extension CTLineBarChartDataProtocol where Set: CTMultiDataSetProtocol { public var maxValue : Double { switch self.chartStyle.topLine { case .maximumValue: - return DataFunctions.multiDataSetMaxValue(from: dataSets) + return self.dataSets.maxValue() case .maximum(of: let value): - return max(DataFunctions.multiDataSetMaxValue(from: dataSets), value) + return max(self.dataSets.maxValue(), value) } } public var average : Double { - return DataFunctions.multiDataSetAverage(from: dataSets) + return self.dataSets.average() } } // MARK: - Y Labels - extension CTLineBarChartDataProtocol { public func getYLabels() -> [Double] { var labels : [Double] = [Double]() From 65322999188e140d2b992d126f53bf41ab57f753 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 5 Mar 2021 09:37:09 +0000 Subject: [PATCH 123/152] Add Final keyword to classes. --- .../Models/ChartData/BarChartData.swift | 32 +++++++-------- .../ChartData/GroupedBarChartData.swift | 32 +++++++-------- .../ChartData/StackedBarChartData.swift | 30 +++++++------- .../Models/ChartData/LineChartData.swift | 40 ++++++++----------- .../Models/ChartData/DoughnutChartData.swift | 18 ++++----- .../Models/ChartData/PieChartData.swift | 20 +++++----- 6 files changed, 82 insertions(+), 90 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index dd07736e..b90fbf87 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -44,17 +44,17 @@ public final class BarChartData: CTBarChartDataProtocol { // MARK: Properties public let id : UUID = UUID() - @Published public var dataSets : BarDataSet - @Published public var metadata : ChartMetadata - @Published public var xAxisLabels : [String]? - @Published public var barStyle : BarStyle - @Published public var chartStyle : BarChartStyle - @Published public var legends : [LegendData] - @Published public var viewData : ChartViewData - @Published public var infoView : InfoViewData = InfoViewData() + @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 var noDataText : Text - public let chartType : (chartType: ChartType, dataSetType: DataSetType) + public final var noDataText : Text + public final let chartType : (chartType: ChartType, dataSetType: DataSetType) // MARK: Initializer /// Initialises a standard Bar Chart. @@ -87,7 +87,7 @@ public final class BarChartData: CTBarChartDataProtocol { } // MARK: Labels - public func getXAxisLabels() -> some View { + public final func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { case .dataPoint: @@ -132,10 +132,10 @@ public final class BarChartData: CTBarChartDataProtocol { } // MARK: - Touch - public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { + public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { self.markerSubView(dataSet: dataSets, touchLocation: touchLocation, chartSize: chartSize) } - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + 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) @@ -144,7 +144,7 @@ public final class BarChartData: CTBarChartDataProtocol { } self.infoView.touchOverlayInfo = points } - public func getPointLocation(dataSet: BarDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { + 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) @@ -154,10 +154,8 @@ public final class BarChartData: CTBarChartDataProtocol { } return nil } - + public typealias Set = BarDataSet public typealias DataPoint = BarChartDataPoint public typealias CTStyle = BarChartStyle } - - diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index ab41f6a5..ecd65c85 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -81,20 +81,20 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { // MARK: Properties public let id : UUID = UUID() - @Published public var dataSets : MultiBarDataSets - @Published public var metadata : ChartMetadata - @Published public var xAxisLabels : [String]? - @Published public var barStyle : BarStyle - @Published public var chartStyle : BarChartStyle - @Published public var legends : [LegendData] - @Published public var viewData : ChartViewData - @Published public var infoView : InfoViewData = InfoViewData() - @Published public var groups : [GroupingData] + @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 var noDataText : Text - public var chartType : (chartType: ChartType, dataSetType: DataSetType) + public final var noDataText : Text + public final var chartType : (chartType: ChartType, dataSetType: DataSetType) - var groupSpacing : CGFloat = 0 + final var groupSpacing : CGFloat = 0 // MARK: Initializer /// Initialises a Grouped Bar Chart. @@ -129,7 +129,7 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { } // MARK: Labels - public func getXAxisLabels() -> some View { + public final func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { case .dataPoint: @@ -177,10 +177,10 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { } } // MARK: Touch - public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { + public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { self.markerSubView(dataSet: dataSets, touchLocation: touchLocation, chartSize: chartSize) } - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [MultiBarChartDataPoint] = [] @@ -205,7 +205,7 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { } self.infoView.touchOverlayInfo = points } - public func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { + 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)) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index 50600300..222eda50 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -77,18 +77,18 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { // MARK: Properties public let id : UUID = UUID() - @Published public var dataSets : MultiBarDataSets - @Published public var metadata : ChartMetadata - @Published public var xAxisLabels : [String]? - @Published public var barStyle : BarStyle - @Published public var chartStyle : BarChartStyle - @Published public var legends : [LegendData] - @Published public var viewData : ChartViewData - @Published public var infoView : InfoViewData = InfoViewData() - @Published public var groups : [GroupingData] + @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 var noDataText : Text - public var chartType : (chartType: ChartType, dataSetType: DataSetType) + public final var noDataText : Text + public final var chartType : (chartType: ChartType, dataSetType: DataSetType) // MARK: Initializer /// Initialises a Grouped Bar Chart. @@ -122,7 +122,7 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { self.setupLegends() } // MARK: Labels - public func getXAxisLabels() -> some View { + public final func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { case .dataPoint: @@ -164,11 +164,11 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { } } // MARK: Touch - public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { + public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { self.markerSubView(dataSet: dataSets, touchLocation: touchLocation, chartSize: chartSize) } - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [MultiBarChartDataPoint] = [] @@ -214,7 +214,7 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { self.infoView.touchOverlayInfo = points } - public func getPointLocation(dataSet: MultiBarDataSets, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { + 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) diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index a786a8bb..b7a4e3c7 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -47,20 +47,20 @@ import SwiftUI public final class LineChartData: CTLineChartDataProtocol { // MARK: Properties - public let id : UUID = UUID() + public final let id : UUID = UUID() - @Published public var dataSets : LineDataSet - @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() + @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 var noDataText : Text - public var chartType : (chartType: ChartType, dataSetType: DataSetType) + public final var noDataText : Text + public final var chartType : (chartType: ChartType, dataSetType: DataSetType) - internal var isFilled : Bool = false + internal final var isFilled : Bool = false // MARK: Initializer /// Initialises a Single Line Chart. @@ -90,7 +90,7 @@ public final class LineChartData: CTLineChartDataProtocol { // , calc : @escaping (LineDataSet) -> LineDataSet // MARK: Labels - public func getXAxisLabels() -> some View { + public final func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { case .dataPoint: @@ -138,7 +138,7 @@ public final class LineChartData: CTLineChartDataProtocol { } // MARK: Points - public func getPointMarker() -> some View { + public final func getPointMarker() -> some View { PointsSubView(dataSets : dataSets, minValue : self.minValue, range : self.range, @@ -146,7 +146,7 @@ public final class LineChartData: CTLineChartDataProtocol { isFilled : self.isFilled) } - public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { + public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { self.markerSubView(dataSet: dataSets, dataPoints: dataSets.dataPoints, lineType: dataSets.style.lineType, @@ -155,7 +155,7 @@ public final class LineChartData: CTLineChartDataProtocol { } // MARK: Accessibility - public func getAccessibility() -> some View { + public final func getAccessibility() -> some View { ForEach(dataSets.dataPoints.indices, id: \.self) { point in AccessibilityRectangle(dataPointCount : self.dataSets.dataPoints.count, @@ -176,7 +176,7 @@ public final class LineChartData: CTLineChartDataProtocol { // MARK: - Touch extension LineChartData { - public func getPointLocation(dataSet: LineDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { + public final func getPointLocation(dataSet: LineDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { let minValue : Double = self.minValue let range : Double = self.range @@ -192,7 +192,7 @@ extension LineChartData { return nil } - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + 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) @@ -202,9 +202,3 @@ extension LineChartData { self.infoView.touchOverlayInfo = points } } - -// MARK: - Legends -extension LineChartData { - - -} diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift index 5f816e3d..df597cf5 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift @@ -33,14 +33,14 @@ public final class DoughnutChartData: CTDoughnutChartDataProtocol { // MARK: Properties public var id : UUID = UUID() - @Published public var dataSets : PieDataSet - @Published public var metadata : ChartMetadata - @Published public var chartStyle : DoughnutChartStyle - @Published public var legends : [LegendData] - @Published public var infoView : InfoViewData + @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 var noDataText: Text - public var chartType : (chartType: ChartType, dataSetType: DataSetType) + public final var noDataText: Text + public final var chartType : (chartType: ChartType, dataSetType: DataSetType) // MARK: Initializer /// Initialises a Doughnut Chart. @@ -67,7 +67,7 @@ public final class DoughnutChartData: CTDoughnutChartDataProtocol { self.makeDataPoints() } - public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { EmptyView() } + public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { EmptyView() } public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint @@ -76,7 +76,7 @@ public final class DoughnutChartData: CTDoughnutChartDataProtocol { // MARK: - Touch extension DoughnutChartData { - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [PieChartDataPoint] = [] let touchDegree = degree(from: touchLocation, in: chartSize) diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift index 84f70183..a37ffc06 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift @@ -33,14 +33,14 @@ public final class PieChartData: CTPieChartDataProtocol { // MARK: Properties public var id : UUID = UUID() - @Published public var dataSets : PieDataSet - @Published public var metadata : ChartMetadata - @Published public var chartStyle : PieChartStyle - @Published public var legends : [LegendData] - @Published public var infoView : InfoViewData + @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 var noDataText: Text - public var chartType: (chartType: ChartType, dataSetType: DataSetType) + public final var noDataText: Text + public final var chartType: (chartType: ChartType, dataSetType: DataSetType) // MARK: Initializer /// Initialises a Pie Chart. @@ -68,7 +68,7 @@ public final class PieChartData: CTPieChartDataProtocol { self.makeDataPoints() } - public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { EmptyView() } + public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { EmptyView() } public typealias Set = PieDataSet public typealias DataPoint = PieChartDataPoint @@ -77,7 +77,7 @@ public final class PieChartData: CTPieChartDataProtocol { // MARK: - Touch extension PieChartData { - public func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { + public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [PieChartDataPoint] = [] let touchDegree = degree(from: touchLocation, in: chartSize) @@ -87,7 +87,7 @@ extension PieChartData { } self.infoView.touchOverlayInfo = points } - public func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { + public final func getPointLocation(dataSet: PieDataSet, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? { return nil } } From 2659f445695dd22352ff00a55a84a18991df1720 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sat, 6 Mar 2021 10:16:18 +0000 Subject: [PATCH 124/152] Refactor Ranged Bar Chart. --- .../Models/ChartData/RangedBarChartData.swift | 34 +- .../BarChart/Views/RangedBarChart.swift | 340 +++++++++++++----- 2 files changed, 276 insertions(+), 98 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift index da7b6c70..c3c2865e 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift @@ -115,7 +115,7 @@ public final class RangedBarChartData: CTRangedBarChartDataProtocol { // MARK: - Touch public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { - self.markerSubView(dataSet: dataSets, touchLocation: touchLocation, chartSize: chartSize) + self.markerSubView() } public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [RangedBarDataPoint] = [] @@ -128,15 +128,13 @@ public final class RangedBarChartData: CTRangedBarChartDataProtocol { } public final func getPointLocation(dataSet: RangedBarDataSet, 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 { - - let upperY = (chartSize.size.height - CGFloat(dataSet.dataPoints[index].upperValue) * ySection) - let lowerY = (chartSize.size.height - CGFloat(dataSet.dataPoints[index].lowerValue) * ySection) + + let value = CGFloat((dataSet.dataPoints[index].upperValue + dataSet.dataPoints[index].lowerValue) / 2) - CGFloat(self.minValue) return CGPoint(x: (CGFloat(index) * xSection) + (xSection / 2), - y: upperY - lowerY) + y: (chartSize.size.height - (value / CGFloat(self.range)) * chartSize.size.height)) } return nil } @@ -144,22 +142,22 @@ public final class RangedBarChartData: CTRangedBarChartDataProtocol { Group { switch self.infoView.touchUnit { case .none: - Text("\(info.upperValue, specifier: self.infoView.touchSpecifier)") - .font(.title3) + Text("\(info.lowerValue, specifier: self.infoView.touchSpecifier) - \(info.upperValue, specifier: self.infoView.touchSpecifier)") + .font(.subheadline) .foregroundColor(self.chartStyle.infoBoxValueColour) Text("\(info.pointDescription ?? "")") .font(.subheadline) .foregroundColor(self.chartStyle.infoBoxDescriptionColour) case .prefix(of: let unit): - Text("\(unit) \(info.upperValue, specifier: self.infoView.touchSpecifier)") - .font(.title3) + Text("\(unit) \(info.lowerValue, specifier: self.infoView.touchSpecifier) - \(info.upperValue, specifier: self.infoView.touchSpecifier)") + .font(.subheadline) .foregroundColor(self.chartStyle.infoBoxValueColour) Text("\(info.pointDescription ?? "")") .font(.subheadline) .foregroundColor(self.chartStyle.infoBoxDescriptionColour) case .suffix(of: let unit): - Text("\(info.upperValue, specifier: self.infoView.touchSpecifier) \(unit)") - .font(.title3) + Text("\(info.lowerValue, specifier: self.infoView.touchSpecifier) - \(info.upperValue, specifier: self.infoView.touchSpecifier) \(unit)") + .font(.subheadline) .foregroundColor(self.chartStyle.infoBoxValueColour) Text("\(info.pointDescription ?? "")") .font(.subheadline) @@ -172,3 +170,15 @@ public final class RangedBarChartData: CTRangedBarChartDataProtocol { 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) + } + + final func getCellAccessibilityValue(dataPoint: RangedBarDataPoint) -> Text { + Text("\(dataPoint.lowerValue, specifier: self.infoView.touchSpecifier) - \(dataPoint.upperValue, specifier: self.infoView.touchSpecifier), \(dataPoint.pointDescription ?? "")") + } +} diff --git a/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift index 92fbfcc5..33d126d1 100644 --- a/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift @@ -17,102 +17,270 @@ public struct RangedBarChart: View where ChartData: RangedBarChartDat self.chartData = chartData } - @State private var startAnimation : Bool = false - public var body: some View { - - HStack(spacing: 0) { - ForEach(chartData.dataSets.dataPoints) { dataPoint in + if chartData.isGreaterThanTwo() { + HStack(spacing: 0) { GeometryReader { geo in - if chartData.barStyle.fillColour.colourType == .colour, - let colour = chartData.barStyle.fillColour.colour { + switch chartData.barStyle.colourFrom { + case .barStyle: - 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: geo.frame(in: .local).midX, - y: getBarPositionX(dataPoint: dataPoint, height: geo.size.height)) - - .background(Color(.gray).opacity(0.000000001)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = false - } - .accessibilityValue(Text("\(dataPoint.upperValue, specifier: chartData.infoView.touchSpecifier), \(dataPoint.pointDescription ?? "")")) - - } else if chartData.barStyle.fillColour.colourType == .gradientColour, - let colours = chartData.barStyle.fillColour.colours, - let startPoint = chartData.barStyle.fillColour.startPoint, - let endPoint = chartData.barStyle.fillColour.endPoint { + RangedBarChartBarStyleSubView(chartData: chartData, barSize: geo.frame(in: .local)) + .accessibilityLabel( Text("\(chartData.metadata.title)")) + case .dataPoints: - 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: geo.frame(in: .local).midX, - y: getBarPositionX(dataPoint: dataPoint, height: geo.size.height)) - - .background(Color(.gray).opacity(0.000000001)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = false - } - .accessibilityValue(Text("\(dataPoint.upperValue, specifier: chartData.infoView.touchSpecifier), \(dataPoint.pointDescription ?? "")")) - - } else if chartData.barStyle.fillColour.colourType == .gradientStops, - let stops = chartData.barStyle.fillColour.stops, - let startPoint = chartData.barStyle.fillColour.startPoint, - let endPoint = chartData.barStyle.fillColour.endPoint { - - let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - - 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: safeStops), - 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: geo.frame(in: .local).midX, - y: getBarPositionX(dataPoint: dataPoint, height: geo.size.height)) - - .background(Color(.gray).opacity(0.000000001)) - .animateOnAppear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = true - } - .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { - self.startAnimation = false - } - .accessibilityValue(Text("\(dataPoint.upperValue, specifier: chartData.infoView.touchSpecifier), \(dataPoint.pointDescription ?? "")")) - + RangedBarChartDataPointSubView(chartData: chartData, barSize: geo.frame(in: .local)) + .accessibilityLabel( Text("\(chartData.metadata.title)")) } - } } + } else { CustomNoDataView(chartData: chartData) } + } +} + +internal struct RangedBarChartBarStyleSubView: View { + + private let chartData : CD + private let barSize : CGRect + + internal init(chartData: CD, barSize: CGRect) { + self.chartData = chartData + self.barSize = barSize + } + + var body: some View { + + if chartData.barStyle.fillColour.colourType == .colour, + let colour = chartData.barStyle.fillColour.colour { + ForEach(chartData.dataSets.dataPoints) { dataPoint in + RangedBarChartColourCell(chartData : chartData, + dataPoint : dataPoint, + colour : colour, + barSize : barSize) + } + } else if chartData.barStyle.fillColour.colourType == .gradientColour, + let colours = chartData.barStyle.fillColour.colours, + let startPoint = chartData.barStyle.fillColour.startPoint, + let endPoint = chartData.barStyle.fillColour.endPoint { + ForEach(chartData.dataSets.dataPoints) { dataPoint in + RangedBarChartColoursCell(chartData : chartData, + dataPoint : dataPoint, + colours : colours, + startPoint: startPoint, + endPoint : endPoint, + barSize : barSize) + } + } else if chartData.barStyle.fillColour.colourType == .gradientStops, + let stops = chartData.barStyle.fillColour.stops, + let startPoint = chartData.barStyle.fillColour.startPoint, + let endPoint = chartData.barStyle.fillColour.endPoint { + + let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) + + ForEach(chartData.dataSets.dataPoints) { dataPoint in + RangedBarChartStopsCell(chartData : chartData, + dataPoint : dataPoint, + stops : safeStops, + startPoint: startPoint, + endPoint : endPoint, + barSize : barSize) + } } } +} + +internal struct RangedBarChartDataPointSubView: View { + + private let chartData : CD + private let barSize : CGRect + + internal init(chartData: CD, barSize: CGRect) { + self.chartData = chartData + self.barSize = barSize + } - private func getBarPositionX(dataPoint: RangedBarDataPoint, height: CGFloat) -> CGFloat { - let value = CGFloat((dataPoint.upperValue + dataPoint.lowerValue) / 2) - CGFloat(chartData.minValue) - return (height - (value / CGFloat(chartData.range)) * height) + internal var body: some View { + + ForEach(chartData.dataSets.dataPoints) { dataPoint in + if dataPoint.fillColour.colourType == .colour, + let colour = dataPoint.fillColour.colour { + + RangedBarChartColourCell(chartData : chartData, + dataPoint : dataPoint, + colour : colour, + barSize : barSize) + + } else if dataPoint.fillColour.colourType == .gradientColour, + let colours = dataPoint.fillColour.colours, + let startPoint = dataPoint.fillColour.startPoint, + let endPoint = dataPoint.fillColour.endPoint { + + RangedBarChartColoursCell(chartData : chartData, + dataPoint : dataPoint, + colours : colours, + startPoint: startPoint, + endPoint : endPoint, + barSize : barSize) + } else if dataPoint.fillColour.colourType == .gradientStops, + let stops = dataPoint.fillColour.stops, + let startPoint = dataPoint.fillColour.startPoint, + let endPoint = dataPoint.fillColour.endPoint { + let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) + + RangedBarChartStopsCell(chartData : chartData, + dataPoint : dataPoint, + stops : safeStops, + startPoint: startPoint, + endPoint : endPoint, + barSize : barSize) + } + } + } +} + +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(chartData.getCellAccessibilityValue(dataPoint: dataPoint)) + } + +} + + +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(chartData.getCellAccessibilityValue(dataPoint: dataPoint)) + } +} + +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(chartData.getCellAccessibilityValue(dataPoint: dataPoint)) } } From 82685bb08abdecf90d3878b5d23c7ca45fcad917 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sat, 6 Mar 2021 10:16:49 +0000 Subject: [PATCH 125/152] Edit layout markers. --- .../BarChart/Models/ChartData/BarChartData.swift | 2 +- .../BarChart/Models/ChartData/GroupedBarChartData.swift | 2 +- .../BarChart/Models/ChartData/StackedBarChartData.swift | 2 +- .../Models/Protocols/BarChartProtocolsExtensions.swift | 9 +++------ 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index b90fbf87..c298b3ac 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -133,7 +133,7 @@ public final class BarChartData: CTBarChartDataProtocol { // MARK: - Touch public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { - self.markerSubView(dataSet: dataSets, touchLocation: touchLocation, chartSize: chartSize) + self.markerSubView() } public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { var points : [BarChartDataPoint] = [] diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index ecd65c85..760a547e 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -178,7 +178,7 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { } // MARK: Touch public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { - self.markerSubView(dataSet: dataSets, touchLocation: touchLocation, chartSize: chartSize) + self.markerSubView() } public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index 222eda50..5f3f6f5d 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -165,7 +165,7 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { } // MARK: Touch public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { - self.markerSubView(dataSet: dataSets, touchLocation: touchLocation, chartSize: chartSize) + self.markerSubView() } public final func getDataPoint(touchLocation: CGPoint, chartSize: CGRect) { diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift index 5d337750..86b802aa 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift @@ -8,14 +8,11 @@ import SwiftUI extension CTBarChartDataProtocol where Self.CTStyle.Mark == BarMarkerType { - internal func markerSubView - (dataSet : DS, - touchLocation : CGPoint, - chartSize : CGRect) -> some View { + internal func markerSubView() -> some View { Group { if let position = self.getPointLocation(dataSet: dataSets as! Self.SetPoint, - touchLocation: touchLocation, - chartSize: chartSize) { + touchLocation: self.infoView.touchLocation, + chartSize: self.infoView.chartSize) { switch self.chartStyle.markerType { case .none: EmptyView() From 71050e4eff6eea46f2520bd1455c90d475f620cb Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sat, 6 Mar 2021 12:08:35 +0000 Subject: [PATCH 126/152] Refactor bar chart views. --- .../BarChartProtocolsExtensions.swift | 6 + .../BarChart/Views/BarChart.swift | 10 +- .../BarChart/Views/GroupedBarChart.swift | 23 +- .../BarChart/Views/RangedBarChart.swift | 135 ++++---- .../Views/SubViews/BarChartSubViews.swift | 117 ++++--- .../BarChart/Views/SubViews/Bars.swift | 307 ++++++++---------- .../ViewModifiers/YAxisLabels.swift | 3 + .../ViewModifiers/YAxisPOI.swift | 29 +- .../Views/ValueLabelCenterSubView.swift | 5 +- .../Views/ValueLabelYAxisSubView.swift | 5 +- 10 files changed, 316 insertions(+), 324 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift index 86b802aa..d71ba928 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift @@ -174,3 +174,9 @@ extension CTMultiBarChartDataProtocol { } } } + +extension CTBarChartDataProtocol { + func getCellAccessibilityValue(dataPoint: DP) -> Text { + Text("\(dataPoint.value, specifier: self.infoView.touchSpecifier), \(dataPoint.pointDescription ?? "")") + } +} diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift index 3c9e1bd4..e9cd7cfd 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift @@ -47,22 +47,18 @@ public struct BarChart: View where ChartData: BarChartData { public var body: some View { if chartData.isGreaterThanTwo() { HStack(spacing: 0) { - ForEach(chartData.dataSets.dataPoints) { dataPoint in - + switch chartData.barStyle.colourFrom { case .barStyle: - BarChartDataSetSubView(chartData: chartData, - dataPoint: dataPoint) + BarChartBarStyleSubView(chartData: chartData) .accessibilityLabel( Text("\(chartData.metadata.title)")) case .dataPoints: - BarChartDataPointSubView(chartData: chartData, - dataPoint: dataPoint) + BarChartDataPointSubView(chartData: chartData) .accessibilityLabel( Text("\(chartData.metadata.title)")) } - } } } else { CustomNoDataView(chartData: chartData) } } diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index 8583d1e2..6a7f232c 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -58,21 +58,27 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD ForEach(chartData.dataSets.dataSets) { dataSet in HStack(spacing: 0) { ForEach(dataSet.dataPoints) { dataPoint in - + if dataPoint.group.fillColour.colourType == .colour, let colour = dataPoint.group.fillColour.colour { - - ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) + + ColourBar(chartData : chartData, + dataPoint : dataPoint, + colour : colour) .accessibilityLabel(Text("\(chartData.metadata.title)")) - + } else if dataPoint.group.fillColour.colourType == .gradientColour, let colours = dataPoint.group.fillColour.colours, let startPoint = dataPoint.group.fillColour.startPoint, let endPoint = dataPoint.group.fillColour.endPoint { - GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) + GradientColoursBar(chartData : chartData, + dataPoint : dataPoint, + colours : colours, + startPoint : startPoint, + endPoint : endPoint) .accessibilityLabel( Text("\(chartData.metadata.title)")) } else if dataPoint.group.fillColour.colourType == .gradientStops, @@ -83,7 +89,12 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) + GradientStopsBar(chartData : chartData, + dataPoint : dataPoint, + stops : safeStops, + startPoint : startPoint, + endPoint : endPoint) + .accessibilityLabel( Text("\(chartData.metadata.title)")) } diff --git a/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift index 33d126d1..425d4204 100644 --- a/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift @@ -20,18 +20,16 @@ public struct RangedBarChart: View where ChartData: RangedBarChartDat public var body: some View { if chartData.isGreaterThanTwo() { HStack(spacing: 0) { - GeometryReader { geo in + + switch chartData.barStyle.colourFrom { + case .barStyle: + + RangedBarChartBarStyleSubView(chartData: chartData) + .accessibilityLabel( Text("\(chartData.metadata.title)")) + case .dataPoints: - switch chartData.barStyle.colourFrom { - case .barStyle: - - RangedBarChartBarStyleSubView(chartData: chartData, barSize: geo.frame(in: .local)) - .accessibilityLabel( Text("\(chartData.metadata.title)")) - case .dataPoints: - - RangedBarChartDataPointSubView(chartData: chartData, barSize: geo.frame(in: .local)) - .accessibilityLabel( Text("\(chartData.metadata.title)")) - } + RangedBarChartDataPointSubView(chartData: chartData) + .accessibilityLabel( Text("\(chartData.metadata.title)")) } } } else { CustomNoDataView(chartData: chartData) } @@ -39,13 +37,11 @@ public struct RangedBarChart: View where ChartData: RangedBarChartDat } internal struct RangedBarChartBarStyleSubView: View { - - private let chartData : CD - private let barSize : CGRect - internal init(chartData: CD, barSize: CGRect) { + private let chartData : CD + + internal init(chartData: CD) { self.chartData = chartData - self.barSize = barSize } var body: some View { @@ -53,22 +49,26 @@ internal struct RangedBarChartBarStyleSubView: View { if chartData.barStyle.fillColour.colourType == .colour, let colour = chartData.barStyle.fillColour.colour { ForEach(chartData.dataSets.dataPoints) { dataPoint in - RangedBarChartColourCell(chartData : chartData, - dataPoint : dataPoint, - colour : colour, - barSize : barSize) + GeometryReader { geo in + RangedBarChartColourCell(chartData : chartData, + dataPoint : dataPoint, + colour : colour, + barSize : geo.frame(in: .local)) + } } } else if chartData.barStyle.fillColour.colourType == .gradientColour, let colours = chartData.barStyle.fillColour.colours, let startPoint = chartData.barStyle.fillColour.startPoint, let endPoint = chartData.barStyle.fillColour.endPoint { ForEach(chartData.dataSets.dataPoints) { dataPoint in - RangedBarChartColoursCell(chartData : chartData, - dataPoint : dataPoint, - colours : colours, - startPoint: startPoint, - endPoint : endPoint, - barSize : barSize) + GeometryReader { geo in + RangedBarChartColoursCell(chartData : chartData, + dataPoint : dataPoint, + colours : colours, + startPoint: startPoint, + endPoint : endPoint, + barSize : geo.frame(in: .local)) + } } } else if chartData.barStyle.fillColour.colourType == .gradientStops, let stops = chartData.barStyle.fillColour.stops, @@ -78,12 +78,14 @@ internal struct RangedBarChartBarStyleSubView: View { let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) ForEach(chartData.dataSets.dataPoints) { dataPoint in - RangedBarChartStopsCell(chartData : chartData, - dataPoint : dataPoint, - stops : safeStops, - startPoint: startPoint, - endPoint : endPoint, - barSize : barSize) + GeometryReader { geo in + RangedBarChartStopsCell(chartData : chartData, + dataPoint : dataPoint, + stops : safeStops, + startPoint: startPoint, + endPoint : endPoint, + barSize : geo.frame(in: .local)) + } } } } @@ -92,47 +94,47 @@ internal struct RangedBarChartBarStyleSubView: View { internal struct RangedBarChartDataPointSubView: View { private let chartData : CD - private let barSize : CGRect - internal init(chartData: CD, barSize: CGRect) { + internal init(chartData: CD) { self.chartData = chartData - self.barSize = barSize } internal var body: some View { ForEach(chartData.dataSets.dataPoints) { dataPoint in - if dataPoint.fillColour.colourType == .colour, - let colour = dataPoint.fillColour.colour { - - RangedBarChartColourCell(chartData : chartData, - dataPoint : dataPoint, - colour : colour, - barSize : barSize) - - } else if dataPoint.fillColour.colourType == .gradientColour, - let colours = dataPoint.fillColour.colours, - let startPoint = dataPoint.fillColour.startPoint, - let endPoint = dataPoint.fillColour.endPoint { - - RangedBarChartColoursCell(chartData : chartData, - dataPoint : dataPoint, - colours : colours, - startPoint: startPoint, - endPoint : endPoint, - barSize : barSize) - } else if dataPoint.fillColour.colourType == .gradientStops, - let stops = dataPoint.fillColour.stops, - let startPoint = dataPoint.fillColour.startPoint, - let endPoint = dataPoint.fillColour.endPoint { - let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - - RangedBarChartStopsCell(chartData : chartData, - dataPoint : dataPoint, - stops : safeStops, - startPoint: startPoint, - endPoint : endPoint, - barSize : barSize) + GeometryReader { geo in + if dataPoint.fillColour.colourType == .colour, + let colour = dataPoint.fillColour.colour { + + RangedBarChartColourCell(chartData : chartData, + dataPoint : dataPoint, + colour : colour, + barSize : geo.frame(in: .local)) + + } else if dataPoint.fillColour.colourType == .gradientColour, + let colours = dataPoint.fillColour.colours, + let startPoint = dataPoint.fillColour.startPoint, + let endPoint = dataPoint.fillColour.endPoint { + + RangedBarChartColoursCell(chartData : chartData, + dataPoint : dataPoint, + colours : colours, + startPoint: startPoint, + endPoint : endPoint, + barSize : geo.frame(in: .local)) + } else if dataPoint.fillColour.colourType == .gradientStops, + let stops = dataPoint.fillColour.stops, + let startPoint = dataPoint.fillColour.startPoint, + let endPoint = dataPoint.fillColour.endPoint { + let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) + + RangedBarChartStopsCell(chartData : chartData, + dataPoint : dataPoint, + stops : safeStops, + startPoint: startPoint, + endPoint : endPoint, + barSize : geo.frame(in: .local)) + } } } } @@ -179,7 +181,6 @@ internal struct RangedBarChartColourCell: View { } .accessibilityValue(chartData.getCellAccessibilityValue(dataPoint: dataPoint)) } - } diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift index 509cfa18..a85feb87 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift @@ -7,28 +7,28 @@ import SwiftUI -// MARK: - Chart Data +// MARK: - Bar Style /** Bar segment where the colour information comes from chart style. */ -internal struct BarChartDataSetSubView: View { +internal struct BarChartBarStyleSubView: View { - private let chartData : CD - private let dataPoint : BarChartDataPoint + private let chartData: CD - internal init(chartData: CD, - dataPoint: BarChartDataPoint - ) { + internal init(chartData: CD) { self.chartData = chartData - self.dataPoint = dataPoint } - + internal var body: some View { if chartData.barStyle.fillColour.colourType == .colour, let colour = chartData.barStyle.fillColour.colour { - ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) + ForEach(chartData.dataSets.dataPoints) { dataPoint in + ColourBar(chartData : chartData, + dataPoint : dataPoint, + colour : colour) + } } else if chartData.barStyle.fillColour.colourType == .gradientColour, let colours = chartData.barStyle.fillColour.colours, @@ -36,18 +36,30 @@ internal struct BarChartDataSetSubView: View { let endPoint = chartData.barStyle.fillColour.endPoint { - GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) - + ForEach(chartData.dataSets.dataPoints) { dataPoint in + GradientColoursBar(chartData : chartData, + dataPoint : dataPoint, + colours : colours, + startPoint : startPoint, + endPoint : endPoint) + } + } else if chartData.barStyle.fillColour.colourType == .gradientStops, let stops = chartData.barStyle.fillColour.stops, let startPoint = chartData.barStyle.fillColour.startPoint, let endPoint = chartData.barStyle.fillColour.endPoint { - + let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) - + ForEach(chartData.dataSets.dataPoints) { dataPoint in + GradientStopsBar(chartData : chartData, + dataPoint : dataPoint, + stops : safeStops, + startPoint : startPoint, + endPoint : endPoint) + } + } } } @@ -57,45 +69,56 @@ internal struct BarChartDataSetSubView: View { Bar segment where the colour information comes from datapoints. */ internal struct BarChartDataPointSubView: View { - - private let chartData : CD - private let dataPoint : BarChartDataPoint - internal init(chartData: CD, - dataPoint: BarChartDataPoint - ) { + private let chartData: CD + + internal init(chartData: CD) { self.chartData = chartData - self.dataPoint = dataPoint } internal var body: some View { - if dataPoint.fillColour.colourType == .colour, - let colour = dataPoint.fillColour.colour - { + ForEach(chartData.dataSets.dataPoints) { dataPoint in - ColourBar(colour, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) - - } else if dataPoint.fillColour.colourType == .gradientColour, - let colours = dataPoint.fillColour.colours, - let startPoint = dataPoint.fillColour.startPoint, - let endPoint = dataPoint.fillColour.endPoint - { - - GradientColoursBar(colours, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) - - } else if dataPoint.fillColour.colourType == .gradientStops, - let stops = dataPoint.fillColour.stops, - let startPoint = dataPoint.fillColour.startPoint, - let endPoint = dataPoint.fillColour.endPoint - { - - let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - - GradientStopsBar(safeStops, startPoint, endPoint, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) - } else { - ColourBar(.blue, dataPoint, chartData.maxValue, chartData.chartStyle, chartData.barStyle.cornerRadius, chartData.barStyle.barWidth, chartData.infoView.touchSpecifier) + if dataPoint.fillColour.colourType == .colour, + let colour = dataPoint.fillColour.colour + { + + ColourBar(chartData : chartData, + dataPoint : dataPoint, + colour : colour) + + } else if dataPoint.fillColour.colourType == .gradientColour, + let colours = dataPoint.fillColour.colours, + let startPoint = dataPoint.fillColour.startPoint, + let endPoint = dataPoint.fillColour.endPoint + { + + GradientColoursBar(chartData : chartData, + dataPoint : dataPoint, + colours : colours, + startPoint : startPoint, + endPoint : endPoint) + + } else if dataPoint.fillColour.colourType == .gradientStops, + let stops = dataPoint.fillColour.stops, + let startPoint = dataPoint.fillColour.startPoint, + let endPoint = dataPoint.fillColour.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) + } } - } } diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift index c170c3cd..c2ec6e1c 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift @@ -13,116 +13,92 @@ import SwiftUI For Standard and Grouped Bar Charts. */ -internal struct ColourBar: View { +internal struct ColourBar: View { + private let chartData : CD private let colour : Color - private let data : DP - private let maxValue : Double - private let chartStyle : BarChartStyle - - private let cornerRadius: CornerRadius - private let barWidth : CGFloat - - private let specifier : String - - internal init(_ colour : Color, - _ dataPoint : DP, - _ maxValue : Double, - _ chartStyle : BarChartStyle, - _ cornerRadius: CornerRadius, - _ barWidth : CGFloat, - _ specifier : String + private let dataPoint : DP + + internal init(chartData : CD, + dataPoint : DP, + colour : Color ) { - self.colour = colour - self.data = dataPoint - self.maxValue = maxValue - self.chartStyle = chartStyle - self.cornerRadius = cornerRadius - self.barWidth = barWidth - self.specifier = specifier + self.chartData = chartData + self.dataPoint = dataPoint + self.colour = colour } @State private var startAnimation : Bool = false internal var body: some View { - RoundedRectangleBarShape(tl: cornerRadius.top, - tr: cornerRadius.top, - bl: cornerRadius.bottom, - br: cornerRadius.bottom) + 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(data.value / maxValue) : 0, anchor: .bottom) - .scaleEffect(x: barWidth, anchor: .center) + .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: chartStyle.globalAnimation) { + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true } - .animateOnDisappear(using: chartStyle.globalAnimation) { + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = false } - .accessibilityValue(Text("\(data.value, specifier: specifier), \(data.pointDescription ?? "")")) + .accessibilityValue(chartData.getCellAccessibilityValue(dataPoint: dataPoint)) } } + + /** Sub view of a single bar using colour gradient. For Standard and Grouped Bar Charts. */ -internal struct GradientColoursBar: View { +internal struct GradientColoursBar: View { + private let chartData : CD + private let dataPoint : DP private let colours : [Color] private let startPoint : UnitPoint private let endPoint : UnitPoint - private let data : DP - private let maxValue : Double - private let chartStyle : BarChartStyle - private let cornerRadius: CornerRadius - private let barWidth : CGFloat - - private let specifier : String - - internal init(_ colours : [Color], - _ startPoint : UnitPoint, - _ endPoint : UnitPoint, - _ data : DP, - _ maxValue : Double, - _ chartStyle : BarChartStyle, - _ cornerRadius: CornerRadius, - _ barWidth : CGFloat, - _ specifier : String + 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 - self.data = data - self.maxValue = maxValue - self.chartStyle = chartStyle - self.cornerRadius = cornerRadius - self.barWidth = barWidth - self.specifier = specifier } @State private var startAnimation : Bool = false internal var body: some View { - RoundedRectangleBarShape(tl: cornerRadius.top, - tr: cornerRadius.top, - bl: cornerRadius.bottom, - br: cornerRadius.bottom) + 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(data.value / maxValue) : 0, anchor: .bottom) - .scaleEffect(x: barWidth, anchor: .center) + .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: chartStyle.globalAnimation) { + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true } - .animateOnDisappear(using: chartStyle.globalAnimation) { + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = false } - .accessibilityValue(Text("\(data.value, specifier: specifier) \(data.pointDescription ?? "")")) + .accessibilityValue(chartData.getCellAccessibilityValue(dataPoint: dataPoint)) } } @@ -131,68 +107,125 @@ internal struct GradientColoursBar: View { +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 - private let data : DP - private let maxValue : Double - private let chartStyle : BarChartStyle - private let cornerRadius: CornerRadius - private let barWidth : CGFloat - - private let specifier : String - - internal init(_ stops : [Gradient.Stop], - _ startPoint : UnitPoint, - _ endPoint : UnitPoint, - _ data : DP, - _ maxValue : Double, - _ chartStyle : BarChartStyle, - _ cornerRadius: CornerRadius, - _ barWidth : CGFloat, - _ specifier : String + 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 - self.data = data - self.maxValue = maxValue - self.chartStyle = chartStyle - self.cornerRadius = cornerRadius - self.barWidth = barWidth - self.specifier = specifier } @State private var startAnimation : Bool = false internal var body: some View { - RoundedRectangleBarShape(tl: cornerRadius.top, - tr: cornerRadius.top, - bl: cornerRadius.bottom, - br: cornerRadius.bottom) + 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(data.value / maxValue) : 0, anchor: .bottom) - .scaleEffect(x: barWidth, anchor: .center) + .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: chartStyle.globalAnimation) { + .animateOnAppear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = true } - .animateOnDisappear(using: chartStyle.globalAnimation) { + .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = false } - .accessibilityValue(Text("\(data.value, specifier: specifier) \(data.pointDescription ?? "")")) + .accessibilityValue(chartData.getCellAccessibilityValue(dataPoint: dataPoint)) } } +// MARK: - Stacked +/** + Individual elements that make up a single bar. + */ +internal struct StackElementSubView: View { + + private let dataSet : MultiBarDataSet + + internal init(dataSet: MultiBarDataSet) { + self.dataSet = dataSet + } + + internal var body: some View { + GeometryReader { geo in + + VStack(spacing: 0) { + ForEach(dataSet.dataPoints.reversed()) { dataPoint in + + if dataPoint.group.fillColour.colourType == .colour, + let colour = dataPoint.group.fillColour.colour + { + + ColourPartBar(colour, getHeight(height : geo.size.height, + dataSet : dataSet, + dataPoint : dataPoint)) + .accessibilityValue(Text("\(dataPoint.value, specifier: "%.f"), \(dataPoint.pointDescription ?? "")")) + + } else if dataPoint.group.fillColour.colourType == .gradientColour, + let colours = dataPoint.group.fillColour.colours, + let startPoint = dataPoint.group.fillColour.startPoint, + let endPoint = dataPoint.group.fillColour.endPoint + { + GradientColoursPartBar(colours, startPoint, endPoint, getHeight(height: geo.size.height, + dataSet : dataSet, + dataPoint : dataPoint)) + .accessibilityValue(Text("\(dataPoint.value, specifier: "%.f") \(dataPoint.pointDescription ?? "")")) + + } else if dataPoint.group.fillColour.colourType == .gradientStops, + let stops = dataPoint.group.fillColour.stops, + let startPoint = dataPoint.group.fillColour.startPoint, + let endPoint = dataPoint.group.fillColour.endPoint + { + + let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) + + GradientStopsPartBar(safeStops, startPoint, endPoint, getHeight(height: geo.size.height, + dataSet : dataSet, + dataPoint : dataPoint)) + .accessibilityValue(Text("\(dataPoint.value, specifier: "%.f") \(dataPoint.pointDescription ?? "")")) + } + + } + } + } + } + + /// 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) + } +} -// MARK: - Grouped /** Sub view of an element of a bar using a single colour. @@ -280,77 +313,3 @@ internal struct GradientStopsPartBar: View { .frame(height: height) } } - -// MARK: - Stacked -/** - Individual elements that make up a single bar. - */ -internal struct StackElementSubView: View { - - private let dataSet : MultiBarDataSet - - internal init(dataSet: MultiBarDataSet) { - self.dataSet = dataSet - } - - internal var body: some View { - GeometryReader { geo in - - VStack(spacing: 0) { - ForEach(dataSet.dataPoints.reversed()) { dataPoint in - - if dataPoint.group.fillColour.colourType == .colour, - let colour = dataPoint.group.fillColour.colour - { - - ColourPartBar(colour, getHeight(height : geo.size.height, - dataSet : dataSet, - dataPoint : dataPoint)) - .accessibilityValue(Text("\(dataPoint.value, specifier: "%.f"), \(dataPoint.pointDescription ?? "")")) - - } else if dataPoint.group.fillColour.colourType == .gradientColour, - let colours = dataPoint.group.fillColour.colours, - let startPoint = dataPoint.group.fillColour.startPoint, - let endPoint = dataPoint.group.fillColour.endPoint - { - - GradientColoursPartBar(colours, startPoint, endPoint, getHeight(height: geo.size.height, - dataSet : dataSet, - dataPoint : dataPoint)) - .accessibilityValue(Text("\(dataPoint.value, specifier: "%.f") \(dataPoint.pointDescription ?? "")")) - - } else if dataPoint.group.fillColour.colourType == .gradientStops, - let stops = dataPoint.group.fillColour.stops, - let startPoint = dataPoint.group.fillColour.startPoint, - let endPoint = dataPoint.group.fillColour.endPoint - { - - let safeStops = GradientStop.convertToGradientStopsArray(stops: stops) - - GradientStopsPartBar(safeStops, startPoint, endPoint, getHeight(height: geo.size.height, - dataSet : dataSet, - dataPoint : dataPoint)) - .accessibilityValue(Text("\(dataPoint.value, specifier: "%.f") \(dataPoint.pointDescription ?? "")")) - } - - } - } - } - } - - /// 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) - } -} - diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift index b26e6958..fde69a6b 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift @@ -70,6 +70,9 @@ internal struct YAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol .onAppear { chartData.infoView.yAxisLabelWidth = geo.frame(in: .local).size.width } +// .onChange(of: geo.frame(in: .local).size.width) { value in +// chartData.infoView.yAxisLabelWidth = value +// } } ) } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index db1e8400..8a20f67b 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -53,6 +53,7 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { self.range = chartData.range self.minValue = chartData.minValue + self.setupPOILegends() } @State private var startAnimation : Bool = false @@ -71,16 +72,6 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = false } - .onAppear { - 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)) - } - } } var marker: some View { @@ -108,8 +99,7 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { specifier : specifier, labelColour : labelColour, labelBackground: labelBackground, - lineColour : lineColour, - chartSize : geo.frame(in: .local)) + lineColour : lineColour) .position(x: -(chartData.infoView.yAxisLabelWidth / 2) - 6, y: getYPoint(chartType: chartData.chartType.chartType, height: geo.size.height)) @@ -124,10 +114,9 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { labelColour : labelColour, labelBackground : labelBackground, lineColour : lineColour, - strokeStyle : strokeStyle, - chartSize : geo.frame(in: .local)) + strokeStyle : strokeStyle) .position(x: geo.frame(in: .local).width / 2, - y: getYPoint(chartType: chartData.chartType.chartType, height: geo.size.height)) + y: getYPoint(chartType: chartData.chartType.chartType, height: geo.size.height)) .accessibilityLabel(Text("P O I Marker")) .accessibilityValue(Text("\(markerName), \(markerValue, specifier: specifier)")) @@ -147,6 +136,16 @@ internal struct YAxisPOI: ViewModifier where T: CTLineBarChartDataProtocol { 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 { diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelCenterSubView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelCenterSubView.swift index 5621c3b1..c65cc48b 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelCenterSubView.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelCenterSubView.swift @@ -16,7 +16,6 @@ internal struct ValueLabelCenterSubView: View where T: CTLineBarChartDataProt private let labelBackground : Color private let lineColour : Color private let strokeStyle : StrokeStyle - private let chartSize : CGRect internal init(chartData : T, markerValue : Double, @@ -24,8 +23,7 @@ internal struct ValueLabelCenterSubView: View where T: CTLineBarChartDataProt labelColour : Color, labelBackground : Color, lineColour : Color, - strokeStyle : StrokeStyle, - chartSize : CGRect + strokeStyle : StrokeStyle ) { self.chartData = chartData self.markerValue = markerValue @@ -34,7 +32,6 @@ internal struct ValueLabelCenterSubView: View where T: CTLineBarChartDataProt self.labelBackground = labelBackground self.lineColour = lineColour self.strokeStyle = strokeStyle - self.chartSize = chartSize } @State private var startAnimation : Bool = false diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelYAxisSubView.swift b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelYAxisSubView.swift index 7fe6f578..f6b9e851 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelYAxisSubView.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Views/ValueLabelYAxisSubView.swift @@ -15,15 +15,13 @@ internal struct ValueLabelYAxisSubView: View where T: CTLineBarChartDataProto private let labelColour : Color private let labelBackground : Color private let lineColour : Color - private let chartSize : CGRect internal init(chartData : T, markerValue : Double, specifier : String, labelColour : Color, labelBackground : Color, - lineColour : Color, - chartSize : CGRect + lineColour : Color ) { self.chartData = chartData self.markerValue = markerValue @@ -31,7 +29,6 @@ internal struct ValueLabelYAxisSubView: View where T: CTLineBarChartDataProto self.labelColour = labelColour self.labelBackground = labelBackground self.lineColour = lineColour - self.chartSize = chartSize } var body: some View { From ebf8c344cf86497298362eb79fef5974f5a8d073 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Mon, 8 Mar 2021 10:04:25 +0000 Subject: [PATCH 127/152] Rename and re organise protocols. Move some functions in to extensions. Add Legend and touch to outward API. --- .../Models/ChartData/BarChartData.swift | 8 +- .../ChartData/GroupedBarChartData.swift | 8 +- .../Models/ChartData/RangedBarChartData.swift | 41 +--- .../ChartData/StackedBarChartData.swift | 4 +- .../BarChart/Models/GroupingData.swift | 2 +- .../Models/Protocols/BarChartProtocols.swift | 25 ++- .../BarChartProtocolsExtensions.swift | 34 ++-- .../BarChart/Views/BarChart.swift | 4 +- .../BarChart/Views/RangedBarChart.swift | 6 +- .../BarChart/Views/StackedBarChart.swift | 2 +- .../BarChart/Views/SubViews/Bars.swift | 22 +- .../Models/ChartData/LineChartData.swift | 24 +-- .../Models/ChartData/MultiLineChartData.swift | 49 ++--- .../ChartData/RangedLineChartData.swift | 18 +- .../Models/Protocols/LineChartProtocols.swift | 3 - .../LineChartProtocolsExtensions.swift | 145 ++++++-------- .../LineChart/Views/RangedLineChart.swift | 22 +- .../PieChart/Views/DoughnutChart.swift | 2 +- .../PieChart/Views/PieChart.swift | 2 +- Sources/SwiftUICharts/Shared/API.swift | 189 ++++++++++++++++++ .../Models/Protocols/SharedProtocols.swift | 42 +--- .../Protocols/SharedProtocolsExtensions.swift | 43 +++- .../SwiftUICharts/Shared/Types/Stroke.swift | 4 +- .../Shared/ViewModifiers/HeaderBox.swift | 11 +- .../Shared/ViewModifiers/InfoBox.swift | 2 +- .../Shared/Views/LegendView.swift | 184 +---------------- .../Shared/Views/TouchOverlayBox.swift | 12 +- .../Protocols/LineAndBarProtocols.swift | 6 + 28 files changed, 438 insertions(+), 476 deletions(-) create mode 100644 Sources/SwiftUICharts/Shared/API.swift diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index c298b3ac..2345aad9 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -96,13 +96,13 @@ public final class BarChartData: CTBarChartDataProtocol { ForEach(dataSets.dataPoints) { data in Spacer() .frame(minWidth: 0, maxWidth: 500) - Text(data.xAxisLabel ?? "") + Text(data.wrappedXAxisLabel) .font(.caption) .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibilityLabel( Text("X Axis Label")) - .accessibilityValue(Text("\(data.xAxisLabel ?? "")")) + .accessibilityLabel(Text("X Axis Label")) + .accessibilityValue(Text("\(data.wrappedXAxisLabel)")) Spacer() .frame(minWidth: 0, maxWidth: 500) } @@ -120,7 +120,7 @@ public final class BarChartData: CTBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibilityLabel( Text("X Axis Label")) + .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) Spacer() .frame(minWidth: 0, maxWidth: 500) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 760a547e..6579d98a 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -139,13 +139,13 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { ForEach(dataSet.dataPoints) { data in Spacer() .frame(minWidth: 0, maxWidth: 500) - Text(data.xAxisLabel ?? "") + Text(data.wrappedXAxisLabel) .font(.caption) .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibilityLabel( Text("XAxisLabel")) - .accessibilityValue(Text("\(data.xAxisLabel ?? "")")) + .accessibilityLabel(Text("XAxisLabel")) + .accessibilityValue(Text("\(data.wrappedXAxisLabel)")) Spacer() .frame(minWidth: 0, maxWidth: 500) } @@ -166,7 +166,7 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibilityLabel( Text("XAxisLabel")) + .accessibilityLabel(Text("XAxisLabel")) .accessibilityValue(Text("\(data)")) Spacer() .frame(minWidth: 0, maxWidth: 500) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift index c3c2865e..4d8af3ca 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift @@ -51,7 +51,7 @@ public final class RangedBarChartData: CTRangedBarChartDataProtocol { self.legends = [LegendData]() self.viewData = ChartViewData() self.chartType = (.bar, .single) -// self.setupLegends() + self.setupLegends() } public final var average : Double { @@ -74,13 +74,13 @@ public final class RangedBarChartData: CTRangedBarChartDataProtocol { ForEach(dataSets.dataPoints) { data in Spacer() .frame(minWidth: 0, maxWidth: 500) - Text(data.xAxisLabel ?? "") + Text(data.wrappedXAxisLabel) .font(.caption) .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibilityLabel( Text("X Axis Label")) - .accessibilityValue(Text("\(data.xAxisLabel ?? "")")) + .accessibilityLabel(Text("X Axis Label")) + .accessibilityValue(Text("\(data.wrappedXAxisLabel)")) Spacer() .frame(minWidth: 0, maxWidth: 500) } @@ -100,7 +100,7 @@ public final class RangedBarChartData: CTRangedBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibilityLabel( Text("X Axis Label")) + .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) if data != labelArray[labelArray.count-1] { Spacer() @@ -138,33 +138,6 @@ public final class RangedBarChartData: CTRangedBarChartDataProtocol { } return nil } - public final func headerTouchOverlaySubView(info: RangedBarDataPoint) -> some View { - Group { - switch self.infoView.touchUnit { - case .none: - Text("\(info.lowerValue, specifier: self.infoView.touchSpecifier) - \(info.upperValue, specifier: self.infoView.touchSpecifier)") - .font(.subheadline) - .foregroundColor(self.chartStyle.infoBoxValueColour) - Text("\(info.pointDescription ?? "")") - .font(.subheadline) - .foregroundColor(self.chartStyle.infoBoxDescriptionColour) - case .prefix(of: let unit): - Text("\(unit) \(info.lowerValue, specifier: self.infoView.touchSpecifier) - \(info.upperValue, specifier: self.infoView.touchSpecifier)") - .font(.subheadline) - .foregroundColor(self.chartStyle.infoBoxValueColour) - Text("\(info.pointDescription ?? "")") - .font(.subheadline) - .foregroundColor(self.chartStyle.infoBoxDescriptionColour) - case .suffix(of: let unit): - Text("\(info.lowerValue, specifier: self.infoView.touchSpecifier) - \(info.upperValue, specifier: self.infoView.touchSpecifier) \(unit)") - .font(.subheadline) - .foregroundColor(self.chartStyle.infoBoxValueColour) - Text("\(info.pointDescription ?? "")") - .font(.subheadline) - .foregroundColor(self.chartStyle.infoBoxDescriptionColour) - } - } - } public typealias Set = RangedBarDataSet public typealias DataPoint = RangedBarDataPoint @@ -177,8 +150,4 @@ extension RangedBarChartData { let value = CGFloat((dataPoint.upperValue + dataPoint.lowerValue) / 2) - CGFloat(self.minValue) return (height - (value / CGFloat(self.range)) * height) } - - final func getCellAccessibilityValue(dataPoint: RangedBarDataPoint) -> Text { - Text("\(dataPoint.lowerValue, specifier: self.infoView.touchSpecifier) - \(dataPoint.upperValue, specifier: self.infoView.touchSpecifier), \(dataPoint.pointDescription ?? "")") - } } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index 5f3f6f5d..b1ba9aa9 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -135,7 +135,7 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibilityLabel( Text("X Axis Label")) + .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(group.title)")) Spacer() @@ -153,7 +153,7 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibilityLabel( Text("X Axis Label")) + .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) Spacer() .frame(minWidth: 0, maxWidth: 500) diff --git a/Sources/SwiftUICharts/BarChart/Models/GroupingData.swift b/Sources/SwiftUICharts/BarChart/Models/GroupingData.swift index f56fbba9..a3a26f47 100644 --- a/Sources/SwiftUICharts/BarChart/Models/GroupingData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/GroupingData.swift @@ -15,7 +15,7 @@ import SwiftUI GroupingData(title: "One", fillColour: ColourStyle(colour: .blue)) ``` */ -public struct GroupingData: Hashable, Identifiable { +public struct GroupingData: CTBarColourProtocol, Hashable, Identifiable { public let id : UUID = UUID() public var title : String diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index 47d4eed1..fb73667f 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -80,7 +80,7 @@ public protocol CTStandardBarChartDataSet: CTSingleDataSetProtocol { public protocol CTMultiBarChartDataSet: CTSingleDataSetProtocol {} -public protocol CTRangedBarChartDataSet: CTSingleDataSetProtocol {} +public protocol CTRangedBarChartDataSet: CTStandardBarChartDataSet {} @@ -89,22 +89,34 @@ public protocol CTRangedBarChartDataSet: CTSingleDataSetProtocol {} // MARK: - DataPoints /** A protocol to extend functionality of `CTLineBarDataPointProtocol` specifically for standard Bar Charts. + + This is base to specify conformance for generics. */ -public protocol CTBarDataPoint: CTLineBarDataPointProtocol {} +public protocol CTBarDataPointBaseProtocol: CTLineBarDataPointProtocol {} /** - A protocol to extend functionality of `CTLineBarDataPointProtocol` specifically for standard Bar Charts. + A protocol to a standard colour scheme for bar charts. */ -public protocol CTStandardBarDataPoint: CTBarDataPoint, CTStandardDataPointProtocol, CTnotRanged { +public protocol CTBarColourProtocol { /// Drawing style of the range fill. var fillColour : ColourStyle { get set } } /** - A protocol to extend functionality of `CTLineBarDataPointProtocol` specifically for multi part Bar Charts. + 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: CTBarDataPoint, CTStandardDataPointProtocol, CTnotRanged { +public protocol CTMultiBarDataPoint: CTBarDataPointBaseProtocol, CTStandardDataPointProtocol, CTnotRanged { /** For grouping data points together so they can be drawn in the correct groupings. @@ -113,5 +125,4 @@ public protocol CTMultiBarDataPoint: CTBarDataPoint, CTStandardDataPointProtocol } -public protocol CTRangedBarDataPoint: CTBarDataPoint, CTRangeDataPointProtocol, CTisRanged {} diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift index d71ba928..df19756b 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift @@ -7,6 +7,7 @@ import SwiftUI +// MARK: - Markers extension CTBarChartDataProtocol where Self.CTStyle.Mark == BarMarkerType { internal func markerSubView() -> some View { Group { @@ -49,13 +50,13 @@ extension CTBarChartDataProtocol where Self.CTStyle.Mark == BarMarkerType { } } - +// MARK: - Legends +// MARK: Standard / Ranged extension CTBarChartDataProtocol where Self.Set.ID == UUID, Self.Set.DataPoint.ID == UUID, Self.Set: CTStandardBarChartDataSet, - Self.Set.DataPoint: CTStandardBarDataPoint { + Self.Set.DataPoint: CTBarColourProtocol { internal func setupLegends() { - switch self.barStyle.colourFrom { case .barStyle: if self.barStyle.fillColour.colourType == .colour, @@ -73,8 +74,8 @@ extension CTBarChartDataProtocol where Self.Set.ID == UUID, self.legends.append(LegendData(id : dataSets.id, legend : dataSets.legendTitle, colour : ColourStyle(colours: colours, - startPoint: .leading, - endPoint: .trailing), + startPoint: .leading, + endPoint: .trailing), strokeStyle: nil, prioity : 1, chartType : .bar)) @@ -84,16 +85,16 @@ extension CTBarChartDataProtocol where Self.Set.ID == UUID, self.legends.append(LegendData(id : dataSets.id, legend : dataSets.legendTitle, colour : ColourStyle(stops: stops, - startPoint: .leading, - endPoint: .trailing), + startPoint: .leading, + endPoint: .trailing), strokeStyle: nil, prioity : 1, chartType : .bar)) } case .dataPoints: - + for data in dataSets.dataPoints { - + if data.fillColour.colourType == .colour, let colour = data.fillColour.colour, let legend = data.pointDescription @@ -111,8 +112,8 @@ extension CTBarChartDataProtocol where Self.Set.ID == UUID, self.legends.append(LegendData(id : data.id, legend : legend, colour : ColourStyle(colours: colours, - startPoint: .leading, - endPoint: .trailing), + startPoint: .leading, + endPoint: .trailing), strokeStyle: nil, prioity : 1, chartType : .bar)) @@ -123,8 +124,8 @@ extension CTBarChartDataProtocol where Self.Set.ID == UUID, self.legends.append(LegendData(id : data.id, legend : legend, colour : ColourStyle(stops: stops, - startPoint: .leading, - endPoint: .trailing), + startPoint: .leading, + endPoint: .trailing), strokeStyle: nil, prioity : 1, chartType : .bar)) @@ -134,6 +135,7 @@ extension CTBarChartDataProtocol where Self.Set.ID == UUID, } } +// MARK: Multi Bar extension CTMultiBarChartDataProtocol { internal func setupLegends() { @@ -174,9 +176,3 @@ extension CTMultiBarChartDataProtocol { } } } - -extension CTBarChartDataProtocol { - func getCellAccessibilityValue(dataPoint: DP) -> Text { - Text("\(dataPoint.value, specifier: self.infoView.touchSpecifier), \(dataPoint.pointDescription ?? "")") - } -} diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift index e9cd7cfd..3ee6ae54 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift @@ -52,12 +52,12 @@ public struct BarChart: View where ChartData: BarChartData { case .barStyle: BarChartBarStyleSubView(chartData: chartData) - .accessibilityLabel( Text("\(chartData.metadata.title)")) + .accessibilityLabel(Text("\(chartData.metadata.title)")) case .dataPoints: BarChartDataPointSubView(chartData: chartData) - .accessibilityLabel( Text("\(chartData.metadata.title)")) + .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 index 425d4204..1ee35ae4 100644 --- a/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift @@ -179,7 +179,7 @@ internal struct RangedBarChartColourCell: View { .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = false } - .accessibilityValue(chartData.getCellAccessibilityValue(dataPoint: dataPoint)) + .accessibilityValue(dataPoint.getCellAccessibilityValue(specifier: chartData.infoView.touchSpecifier)) } } @@ -231,7 +231,7 @@ internal struct RangedBarChartColoursCell: View { .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = false } - .accessibilityValue(chartData.getCellAccessibilityValue(dataPoint: dataPoint)) + .accessibilityValue(dataPoint.getCellAccessibilityValue(specifier: chartData.infoView.touchSpecifier)) } } @@ -282,6 +282,6 @@ internal struct RangedBarChartStopsCell: View { .animateOnDisappear(using: chartData.chartStyle.globalAnimation) { self.startAnimation = false } - .accessibilityValue(chartData.getCellAccessibilityValue(dataPoint: dataPoint)) + .accessibilityValue(dataPoint.getCellAccessibilityValue(specifier: chartData.infoView.touchSpecifier)) } } diff --git a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift index 126cb208..023fdace 100644 --- a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift @@ -54,7 +54,7 @@ public struct StackedBarChart: View where ChartData: StackedBarChartD HStack(alignment: .bottom, spacing: 0) { ForEach(chartData.dataSets.dataSets) { dataSet in - StackElementSubView(dataSet: dataSet) + 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)) diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift index c2ec6e1c..1b5b7e6f 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift @@ -14,7 +14,7 @@ import SwiftUI For Standard and Grouped Bar Charts. */ internal struct ColourBar: View { + DP: CTStandardDataPointProtocol & CTBarDataPointBaseProtocol>: View { private let chartData : CD private let colour : Color @@ -46,7 +46,7 @@ internal struct ColourBar: View { + DP: CTStandardDataPointProtocol & CTBarDataPointBaseProtocol>: View { private let chartData : CD private let dataPoint : DP @@ -98,7 +98,7 @@ internal struct GradientColoursBar: View { + DP: CTStandardDataPointProtocol & CTBarDataPointBaseProtocol>: View { private let chartData : CD private let dataPoint : DP @@ -148,7 +148,7 @@ internal struct GradientStopsBar = InfoViewData() - + public final var noDataText : Text public final var chartType : (chartType: ChartType, dataSetType: DataSetType) @@ -103,8 +103,8 @@ public final class LineChartData: CTLineChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibilityLabel( Text("X Axis Label")) - .accessibilityValue(Text("\(data.xAxisLabel ?? "")")) + .accessibilityLabel(Text("X Axis Label")) + .accessibilityValue(Text("\(data.wrappedXAxisLabel)")) } if data != self.dataSets.dataPoints[self.dataSets.dataPoints.count - 1] { Spacer() @@ -123,7 +123,7 @@ public final class LineChartData: CTLineChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibilityLabel( Text("X Axis Label")) + .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) if data != labelArray[labelArray.count - 1] { Spacer() @@ -153,21 +153,6 @@ public final class LineChartData: CTLineChartDataProtocol { touchLocation: touchLocation, chartSize: chartSize) } - - // MARK: Accessibility - public final 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(Text(String(format: self.infoView.touchSpecifier, - self.dataSets.dataPoints[point].value) + - ", \(self.dataSets.dataPoints[point].pointDescription ?? "")")) - } - } public typealias Set = LineDataSet public typealias DataPoint = LineChartDataPoint @@ -193,6 +178,7 @@ extension LineChartData { } 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) diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index 2ce375eb..3bfaaccc 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -56,18 +56,18 @@ public final class MultiLineChartData: CTLineChartDataProtocol { // MARK: Properties public let id : UUID = UUID() - @Published public var dataSets : MultiLineDataSet - @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() + @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 var noDataText : Text - public var chartType : (chartType: ChartType, dataSetType: DataSetType) + public final var noDataText : Text + public final var chartType : (chartType: ChartType, dataSetType: DataSetType) - internal var isFilled : Bool = false + internal final var isFilled : Bool = false // MARK: Initializers /// Initialises a Multi Line Chart. @@ -96,7 +96,7 @@ public final class MultiLineChartData: CTLineChartDataProtocol { } // MARK: Labels - public func getXAxisLabels() -> some View { + public final func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { case .dataPoint: @@ -109,8 +109,8 @@ public final class MultiLineChartData: CTLineChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibilityLabel( Text("X Axis Label")) - .accessibilityValue(Text("\(data.xAxisLabel ?? "")")) + .accessibilityLabel(Text("X Axis Label")) + .accessibilityValue(Text("\(data.wrappedXAxisLabel)")) } if data != self.dataSets.dataSets[0].dataPoints[self.dataSets.dataSets[0].dataPoints.count - 1] { Spacer() @@ -130,7 +130,7 @@ public final class MultiLineChartData: CTLineChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibilityLabel( Text("X Axis Label")) + .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) if data != labelArray[labelArray.count - 1] { Spacer() @@ -145,8 +145,8 @@ public final class MultiLineChartData: CTLineChartDataProtocol { } // MARK: Points - public func getPointMarker() -> some View { - ForEach(self.dataSets.dataSets, id: \.self) { dataSet in + public final func getPointMarker() -> some View { + ForEach(self.dataSets.dataSets, id: \.id) { dataSet in PointsSubView(dataSets : dataSet, minValue : self.minValue, range : self.range, @@ -155,9 +155,9 @@ public final class MultiLineChartData: CTLineChartDataProtocol { } } - public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { + public final func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { ZStack { - ForEach(self.dataSets.dataSets, id: \.self) { dataSet in + ForEach(self.dataSets.dataSets, id: \.id) { dataSet in self.markerSubView(dataSet: dataSet, dataPoints: dataSet.dataPoints, lineType: dataSet.style.lineType, @@ -169,19 +169,13 @@ public final class MultiLineChartData: CTLineChartDataProtocol { // 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(Text(String(format: self.infoView.touchSpecifier, - dataSet.dataPoints[point].value) + - ", \(dataSet.dataPoints[point].pointDescription ?? "")")) + .accessibilityLabel(Text("\(self.metadata.title)")) + .accessibilityValue(dataSet.dataPoints[point].getCellAccessibilityValue(specifier: self.infoView.touchSpecifier)) } } } @@ -189,14 +183,11 @@ public final class MultiLineChartData: CTLineChartDataProtocol { 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 diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift index d1073eb3..ff1dc2ca 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift @@ -72,8 +72,8 @@ public final class RangedLineChartData: CTLineChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibilityLabel( Text("X Axis Label")) - .accessibilityValue(Text("\(data.xAxisLabel ?? "")")) + .accessibilityLabel(Text("X Axis Label")) + .accessibilityValue(Text("\(data.wrappedXAxisLabel)")) } if data != self.dataSets.dataPoints[self.dataSets.dataPoints.count - 1] { Spacer() @@ -92,7 +92,7 @@ public final class RangedLineChartData: CTLineChartDataProtocol { .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) .minimumScaleFactor(0.5) - .accessibilityLabel( Text("X Axis Label")) + .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) if data != labelArray[labelArray.count - 1] { Spacer() @@ -157,32 +157,26 @@ public final class RangedLineChartData: CTLineChartDataProtocol { Text("\(info.upperValue, specifier: self.infoView.touchSpecifier)") .font(.title3) .foregroundColor(self.chartStyle.infoBoxValueColour) - Text("\(info.pointDescription ?? "")") + Text("\(info.wrappedDescription)") .font(.subheadline) .foregroundColor(self.chartStyle.infoBoxDescriptionColour) case .prefix(of: let unit): Text("\(unit) \(info.upperValue, specifier: self.infoView.touchSpecifier)") .font(.title3) .foregroundColor(self.chartStyle.infoBoxValueColour) - Text("\(info.pointDescription ?? "")") + Text("\(info.wrappedDescription)") .font(.subheadline) .foregroundColor(self.chartStyle.infoBoxDescriptionColour) case .suffix(of: let unit): Text("\(info.upperValue, specifier: self.infoView.touchSpecifier) \(unit)") .font(.title3) .foregroundColor(self.chartStyle.infoBoxValueColour) - Text("\(info.pointDescription ?? "")") + Text("\(info.wrappedDescription)") .font(.subheadline) .foregroundColor(self.chartStyle.infoBoxDescriptionColour) } } } - - // MARK: Accessibility - public func getAccessibility() -> some View { - EmptyView() - } - public typealias Set = RangedLineDataSet public typealias DataPoint = RangedLineChartDataPoint diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index d8da1c58..9961b10f 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -33,9 +33,6 @@ public protocol CTLineChartDataProtocol: CTLineBarChartDataProtocol { func getAccessibility() -> Access } - - - // MARK: - Style /** A protocol to extend functionality of `CTLineBarChartStyle` specifically for Line Charts. diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index 346513a2..490942b0 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -11,20 +11,20 @@ import SwiftUI extension CTLineChartDataProtocol { public static func getIndicatorLocation(rect: CGRect, - dataPoints: [DP], - touchLocation: CGPoint, - lineType: LineType, - minValue: Double, - range: Double + dataPoints: [DP], + touchLocation: CGPoint, + lineType: LineType, + minValue: Double, + range: Double ) -> CGPoint { let path = Self.getPath(lineType : lineType, - rect : rect, - dataPoints : dataPoints, - minValue : minValue, - range : range, - touchLocation: touchLocation, - isFilled : false) + rect : rect, + dataPoints : dataPoints, + minValue : minValue, + range : range, + touchLocation: touchLocation, + isFilled : false) return Self.locationOnPath(Self.getPercentageOfPath(path: path, touchLocation: touchLocation), path) } } @@ -465,40 +465,52 @@ internal struct IndicatorSwitch: View { // MARK: - Legends extension CTLineChartDataProtocol where Self.Set.ID == UUID, - Self.Set: CTLineChartDataSet { - internal func setupLegends() { - - if dataSets.style.lineColour.colourType == .colour, - let colour = dataSets.style.lineColour.colour + 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 : dataSets.id, - legend : dataSets.legendTitle, + self.legends.append(LegendData(id : dataSet.id, + legend : dataSet.legendTitle, colour : ColourStyle(colour: colour), - strokeStyle: dataSets.style.strokeStyle, + strokeStyle: dataSet.style.strokeStyle, prioity : 1, chartType : .line)) - - } else if dataSets.style.lineColour.colourType == .gradientColour, - let colours = dataSets.style.lineColour.colours + + } else if dataSet.style.lineColour.colourType == .gradientColour, + let colours = dataSet.style.lineColour.colours { - self.legends.append(LegendData(id : dataSets.id, - legend : dataSets.legendTitle, + self.legends.append(LegendData(id : dataSet.id, + legend : dataSet.legendTitle, colour : ColourStyle(colours: colours, - startPoint: .leading, - endPoint: .trailing), - strokeStyle: dataSets.style.strokeStyle, + startPoint: .leading, + endPoint: .trailing), + strokeStyle: dataSet.style.strokeStyle, prioity : 1, chartType : .line)) - - } else if dataSets.style.lineColour.colourType == .gradientStops, - let stops = dataSets.style.lineColour.stops + + } else if dataSet.style.lineColour.colourType == .gradientStops, + let stops = dataSet.style.lineColour.stops { - self.legends.append(LegendData(id : dataSets.id, - legend : dataSets.legendTitle, + self.legends.append(LegendData(id : dataSet.id, + legend : dataSet.legendTitle, colour : ColourStyle(stops: stops, - startPoint: .leading, - endPoint: .trailing), - strokeStyle: dataSets.style.strokeStyle, + startPoint: .leading, + endPoint: .trailing), + strokeStyle: dataSet.style.strokeStyle, prioity : 1, chartType : .line)) } @@ -517,70 +529,45 @@ extension CTLineChartDataProtocol where Self.Set.ID == UUID, 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), + startPoint: .leading, + endPoint: .trailing), strokeStyle: dataSets.style.strokeStyle, prioity : 1, - chartType : .line)) - + 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), + startPoint: .leading, + endPoint: .trailing), strokeStyle: dataSets.style.strokeStyle, prioity : 1, - chartType : .line)) + chartType : .bar)) } } } -extension CTLineChartDataProtocol where Self.Set == MultiLineDataSet { - internal func setupLegends() { - for dataSet in dataSets.dataSets { - 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)) + +// 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) - } 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)) - } + .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/Views/RangedLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift index 0bd7a051..2881c01f 100644 --- a/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift @@ -24,7 +24,7 @@ public struct RangedLineChart: View where ChartData: RangedLineChartD ZStack { chartData.getAccessibility() - + // MARK: Ranged Box if chartData.dataSets.style.fillColour.colourType == .colour, let colour = chartData.dataSets.style.fillColour.colour { @@ -66,7 +66,7 @@ public struct RangedLineChart: View where ChartData: RangedLineChartD endPoint: endPoint)) } - + // MARK: Main Line if chartData.dataSets.style.lineColour.colourType == .colour, let colour = chartData.dataSets.style.lineColour.colour { @@ -114,21 +114,3 @@ public struct RangedLineChart: View where ChartData: RangedLineChartD } else { CustomNoDataView(chartData: chartData) } } } - - -/* - - ZStack { - RangedLineFillShape(dataPoints: chartData.dataSets.dataPoints, - lineType: chartData.dataSets.style.lineType, - minValue: chartData.minValue, - range: chartData.range) - .fill(Color.red.opacity(0.25)) - LineShape(dataPoints: chartData.dataSets.dataPoints, - lineType: chartData.dataSets.style.lineType, - isFilled: false, - minValue: chartData.minValue, - range: chartData.range) - .stroke(Color.blue, lineWidth: 3) - } - */ diff --git a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift index 0c12e65f..4aef5b41 100644 --- a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift @@ -58,7 +58,7 @@ public struct DoughnutChart: View where ChartData: DoughnutChartData .shadow(color: Color.primary, radius: 10) } .accessibilityLabel(Text("\(chartData.metadata.title)")) - .accessibilityValue(Text(String(format: chartData.infoView.touchSpecifier, chartData.dataSets.dataPoints[data].value) + "\(chartData.dataSets.dataPoints[data].pointDescription ?? "")")) + .accessibilityValue(chartData.dataSets.dataPoints[data].getCellAccessibilityValue(specifier: chartData.infoView.touchSpecifier)) } } .animateOnAppear(using: chartData.chartStyle.globalAnimation) { diff --git a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift index 3250a560..5f402eb0 100644 --- a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift @@ -58,7 +58,7 @@ public struct PieChart: View where ChartData: PieChartData { .shadow(color: Color.primary, radius: 10) } .accessibilityLabel(Text("\(chartData.metadata.title)")) - .accessibilityValue(Text(String(format: chartData.infoView.touchSpecifier, chartData.dataSets.dataPoints[data].value) + "\(chartData.dataSets.dataPoints[data].pointDescription ?? "")")) + .accessibilityValue(chartData.dataSets.dataPoints[data].getCellAccessibilityValue(specifier: chartData.infoView.touchSpecifier)) } } diff --git a/Sources/SwiftUICharts/Shared/API.swift b/Sources/SwiftUICharts/Shared/API.swift new file mode 100644 index 00000000..bdae5eac --- /dev/null +++ b/Sources/SwiftUICharts/Shared/API.swift @@ -0,0 +1,189 @@ +// +// API.swift +// +// +// Created by Will Dale on 07/03/2021. +// + +import SwiftUI + +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.infoValue(info: point) + } + } +} + +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) + } + } +} + +extension LegendData { + 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) + } + } + } + } + } + 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/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 5bb9da11..4bafc8a7 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -123,44 +123,14 @@ public protocol CTChartData: ObservableObject, Identifiable { */ func getPointLocation(dataSet: SetPoint, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? - associatedtype TouchInformation: View - func headerTouchOverlaySubView(info: DataPoint) -> TouchInformation -} - -extension CTChartData where Self.DataPoint : CTStandardDataPointProtocol { - public func headerTouchOverlaySubView(info: Self.DataPoint) -> some View { - Group { - switch self.infoView.touchUnit { - case .none: - Text("\(info.value, specifier: self.infoView.touchSpecifier)") - .font(.title3) - .foregroundColor(self.chartStyle.infoBoxValueColour) - Text("\(info.pointDescription ?? "")") - .font(.subheadline) - .foregroundColor(self.chartStyle.infoBoxDescriptionColour) - case .prefix(of: let unit): - Text("\(unit) \(info.value, specifier: self.infoView.touchSpecifier)") - .font(.title3) - .foregroundColor(self.chartStyle.infoBoxValueColour) - Text("\(info.pointDescription ?? "")") - .font(.subheadline) - .foregroundColor(self.chartStyle.infoBoxDescriptionColour) - case .suffix(of: let unit): - Text("\(info.value, specifier: self.infoView.touchSpecifier) \(unit)") - .font(.title3) - .foregroundColor(self.chartStyle.infoBoxValueColour) - Text("\(info.pointDescription ?? "")") - .font(.subheadline) - .foregroundColor(self.chartStyle.infoBoxDescriptionColour) - } - } - } + associatedtype InfoValue : View + associatedtype InfoDesc : View + + func infoValue(info: DataPoint) -> InfoValue + func infoDescription(info: DataPoint) -> InfoDesc } - - - // MARK: - Data Sets /** Main protocol to set conformace for types of Data Sets. @@ -219,6 +189,8 @@ public protocol CTDataPointBaseProtocol: Hashable, Identifiable { Date can be used for optionally performing additional calculations. */ var date : Date? { get set } + + func valueAsString(specifier: String) -> String } /** diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift index dfc94328..fb00ece4 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift @@ -31,9 +31,20 @@ extension CTChartData { self.getDataPoint(touchLocation: touchLocation, chartSize: chartSize) } } + extension CTChartData { - public func legendOrder() -> [LegendData] { - return legends.sorted { $0.prioity < $1.prioity} + public func infoValue(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)") + } + } + public func infoDescription(info: DataPoint) -> some View { + Text("\(info.wrappedDescription)") } } @@ -111,3 +122,31 @@ extension CTMultiDataSetProtocol where Self.DataSet.DataPoint: CTStandardDataPoi return sum / Double(setHolder.count) } } + +// MARK: - Data Point +extension CTDataPointBaseProtocol { + func getCellAccessibilityValue(specifier: String) -> Text { + Text(self.valueAsString(specifier: specifier) + ", " + self.wrappedDescription) + } +} + +extension CTDataPointBaseProtocol { + public var wrappedDescription : String { + self.pointDescription ?? "" + } +} +extension CTStandardDataPointProtocol { + public func valueAsString(specifier: String) -> String { + String(format: specifier, self.value) + } +} +extension CTRangeDataPointProtocol { + public func valueAsString(specifier: String) -> String { + String(format: specifier, self.lowerValue) + "-" + String(format: specifier, self.upperValue) + } +} +extension CTRangedLineDataPoint { + public func valueAsString(specifier: String) -> String { + String(format: specifier, self.lowerValue) + "-" + String(format: specifier, self.upperValue) + } +} diff --git a/Sources/SwiftUICharts/Shared/Types/Stroke.swift b/Sources/SwiftUICharts/Shared/Types/Stroke.swift index f07c4f66..eb562003 100644 --- a/Sources/SwiftUICharts/Shared/Types/Stroke.swift +++ b/Sources/SwiftUICharts/Shared/Types/Stroke.swift @@ -41,7 +41,7 @@ public struct Stroke: Hashable, Identifiable { extension Stroke { /// Convert `Stroke` to `StrokeStyle` - func strokeToStrokeStyle() -> StrokeStyle { + internal func strokeToStrokeStyle() -> StrokeStyle { StrokeStyle(lineWidth : self.lineWidth, lineCap : self.lineCap, lineJoin : self.lineJoin, @@ -53,7 +53,7 @@ extension Stroke { extension StrokeStyle { /// Convert `StrokeStyle` to `Stroke` - func toStroke() -> Stroke { + internal func toStroke() -> Stroke { Stroke(lineWidth : self.lineWidth, lineCap : self.lineCap, lineJoin : self.lineJoin, diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index a058e25a..6c416612 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -33,9 +33,16 @@ internal struct HeaderBox: ViewModifier where T: CTChartData { VStack(alignment: .trailing) { if chartData.infoView.isTouchCurrent { - ForEach(chartData.infoView.touchOverlayInfo, id: \.self) { info in + ForEach(chartData.infoView.touchOverlayInfo, id: \.id) { point in + + chartData.infoValue(info: point) + .font(.title3) + .foregroundColor(chartData.chartStyle.infoBoxValueColour) + + chartData.infoDescription(info: point) + .font(.subheadline) + .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) - chartData.headerTouchOverlaySubView(info: info) } } else { Text("") diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift index ead3c232..2b809ceb 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift @@ -14,7 +14,7 @@ internal struct InfoBox: ViewModifier where T: CTChartData { @ObservedObject var chartData: T - @State private var boxFrame : CGRect = CGRect(x: 0, y: 0, width: 0, height: 50) + @State private var boxFrame: CGRect = CGRect(x: 0, y: 0, width: 0, height: 50) internal func body(content: Content) -> some View { VStack { diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index a0c26369..31efa25d 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -28,54 +28,14 @@ internal struct LegendView: View where T: CTChartData { internal var body: some View { LazyVGrid(columns: columns, alignment: .leading) { - ForEach(chartData.legends) { legend in + ForEach(chartData.legends, id: \.id) { legend in - switch legend.chartType { - - case .line: + legend.getLegend(textColor: textColor) + .if(scaleLegendBar(legend: legend)) { $0.scaleEffect(1.2, anchor: .leading) } + .if(scaleLegendPie(legend: legend)) {$0.scaleEffect(1.2, anchor: .leading) } - line(legend) - .accessibilityLabel( Text(accessibilityLegendLabel(legend: legend))) - .accessibilityValue(Text("\(legend.legend)")) - - case .bar: - - bar(legend) - .if(scaleLegendBar(legend: legend)) { $0.scaleEffect(1.2, anchor: .leading) } - .accessibilityLabel( Text(accessibilityLegendLabel(legend: legend))) - .accessibilityValue(Text("\(legend.legend)")) - case .pie: - - pie(legend) - .if(scaleLegendPie(legend: legend)) { - $0.scaleEffect(1.2, anchor: .leading) - } - .accessibilityLabel( Text(accessibilityLegendLabel(legend: legend))) - .accessibilityValue(Text("\(legend.legend)")) - } - } - }.id(UUID()) - } - - private func accessibilityLegendLabel(legend: LegendData) -> String { - switch legend.chartType { - case .line: - if legend.prioity == 1 { - return "Line Chart Legend" - } else { - return "P O I Marker Legend" - } - case .bar: - if legend.prioity == 1 { - return "Bar Chart Legend" - } else { - return "P O I Marker Legend" - } - case .pie: - if legend.prioity == 1 { - return "Pie Chart Legend" - } else { - return "P O I Marker Legend" + .accessibilityLabel(Text(legend.accessibilityLegendLabel())) + .accessibilityValue(Text("\(legend.legend)")) } } } @@ -112,136 +72,4 @@ internal struct LegendView: View where T: CTChartData { return false } } - - /// Returns a Line legend. - private func line(_ legend: LegendData) -> some View { - Group { - if let stroke = legend.strokeStyle { - let strokeStyle = stroke.strokeToStrokeStyle() - if let colour = legend.colour.colour { - HStack { - LegendLine(width: 40) - .stroke(colour, style: strokeStyle) - .frame(width: 40, height: 3) - Text(legend.legend) - .font(.caption) - .foregroundColor(textColor) - } - - } else if let colours = legend.colour.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) - .foregroundColor(textColor) - } - } else if let stops = legend.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(legend.legend) - .font(.caption) - .foregroundColor(textColor) - } - } - } - } - } - - /// Returns a Bar legend. - private func bar(_ legend: LegendData) -> some View { - Group { - if let colour = legend.colour.colour - { - HStack { - Rectangle() - .fill(colour) - .frame(width: 20, height: 20) - Text(legend.legend) - .font(.caption) - } - } else if let colours = legend.colour.colours, - let startPoint = legend.colour.startPoint, - let endPoint = legend.colour.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.colour.stops, - let startPoint = legend.colour.startPoint, - let endPoint = legend.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(legend.legend) - .font(.caption) - } - } - } - } - - /// Returns a Pie legend. - private func pie(_ legend: LegendData) -> some View { - Group { - if let colour = legend.colour.colour { - HStack { - Circle() - .fill(colour) - .frame(width: 20, height: 20) - Text(legend.legend) - .font(.caption) - } - - } else if let colours = legend.colour.colours, - let startPoint = legend.colour.startPoint, - let endPoint = legend.colour.endPoint - { - HStack { - Circle() - .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.colour.stops, - let startPoint = legend.colour.startPoint, - let endPoint = legend.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(legend.legend) - .font(.caption) - } - } - } - } } diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index 0257247e..075b245e 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -26,10 +26,16 @@ internal struct TouchOverlayBox: View { internal var body: some View { HStack { - ForEach(chartData.infoView.touchOverlayInfo, id: \.self) { point in + ForEach(chartData.infoView.touchOverlayInfo, id: \.id) { point in + + chartData.infoValue(info: point) + .font(.subheadline) + .foregroundColor(chartData.chartStyle.infoBoxValueColour) + + chartData.infoDescription(info: point) + .font(.subheadline) + .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) - chartData.headerTouchOverlaySubView(info: point) - } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index a42d10b0..59e593d7 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -151,3 +151,9 @@ public protocol CTLineBarDataPointProtocol: CTDataPointBaseProtocol { */ var xAxisLabel: String? { get set } } + +extension CTLineBarDataPointProtocol { + var wrappedXAxisLabel : String { + self.xAxisLabel ?? "" + } +} From 60221544cc57c09969062407e0d783299a49820e Mon Sep 17 00:00:00 2001 From: Will Dale Date: Thu, 11 Mar 2021 09:13:48 +0000 Subject: [PATCH 128/152] Tidy Up. --- Package.swift | 1 + .../BarChart/Extras/BarChartEnums.swift | 11 +- .../Models/ChartData/BarChartData.swift | 87 ++++++++------- .../ChartData/GroupedBarChartData.swift | 95 ++++------------ .../Models/ChartData/RangedBarChartData.swift | 8 +- .../ChartData/StackedBarChartData.swift | 7 +- .../Models/DataSet/MultiBarDataSets.swift | 20 ++-- .../Models/Datapoints/BarChartDataPoint.swift | 32 +++--- .../Datapoints/MultiBarChartDataPoint.swift | 35 +++--- .../Datapoints/RangedBarDataPoint.swift | 36 +++---- .../BarChart/Models/GroupingData.swift | 18 ++-- .../Models/Protocols/BarChartProtocols.swift | 6 +- .../BarChartProtocolsExtensions.swift | 42 ++++---- .../BarChart/Models/Style/BarChartStyle.swift | 8 +- .../BarChart/Models/Style/BarStyle.swift | 17 ++- .../BarChart/Views/GroupedBarChart.swift | 20 ++-- .../BarChart/Views/RangedBarChart.swift | 40 +++---- .../Views/SubViews/BarChartSubViews.swift | 40 +++---- .../BarChart/Views/SubViews/Bars.swift | 20 ++-- .../Models/ChartData/LineChartData.swift | 16 +-- .../Models/ChartData/MultiLineChartData.swift | 22 ++-- .../ChartData/RangedLineChartData.swift | 22 ++-- .../DataPoints/LineChartDataPoint.swift | 37 +++---- .../DataPoints/RangedLineChartDataPoint.swift | 34 +++--- .../Models/Style/LineChartStyle.swift | 10 +- .../Models/ChartData/DoughnutChartData.swift | 10 +- .../Models/ChartData/PieChartData.swift | 10 +- .../Models/DataPoints/PieChartDataPoint.swift | 32 +++--- .../PieChart/Models/DataSets/PieDataSet.swift | 10 +- .../PieChartProtocolsExtentions.swift | 2 +- Sources/SwiftUICharts/Shared/API.swift | 20 ++++ .../Shared/Extras/SharedEnums.swift | 2 +- .../Shared/Models/InfoViewData.swift | 2 +- .../Models/Protocols/SharedProtocols.swift | 75 +++++++++---- .../Protocols/SharedProtocolsExtensions.swift | 46 ++++++-- .../Shared/ViewModifiers/TouchOverlay.swift | 6 +- .../Shared/Views/LegendView.swift | 2 +- .../Shared/Views/TouchOverlayBox.swift | 16 +-- .../Extras/LineAndBarEnums.swift | 4 +- .../Models/ChartViewData.swift | 5 +- .../Protocols/LineAndBarProtocols.swift | 23 ++-- .../LineAndBarProtocolsExtentions.swift | 102 +----------------- .../ViewModifiers/XAxisLabels.swift | 21 +++- .../ViewModifiers/YAxisLabels.swift | 40 ++++--- .../BarCharts/GroupedBarChartTests.swift | 74 ++++++++----- .../BarCharts/StackedBarChartTests.swift | 74 ++++++++----- .../LineCharts/LineChartTests.swift | 53 ++++----- .../LineCharts/MultiLineChartTest.swift | 56 ++++------ 48 files changed, 664 insertions(+), 705 deletions(-) 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/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift b/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift index 15762f18..590fbe48 100644 --- a/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift +++ b/Sources/SwiftUICharts/BarChart/Extras/BarChartEnums.swift @@ -25,12 +25,11 @@ public enum ColourFrom { ``` case none // No overlay markers. case vertical // Vertical line from top to bottom. - case full // 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. - + 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 { diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index 2345aad9..b8e28c71 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -10,35 +10,7 @@ import SwiftUI /** Data for drawing and styling a standard Bar Chart. - # Example - ``` - static func weekOfData() -> BarChartData { - - let data : BarDataSet = - BarDataSet(dataPoints: [ - BarChartDataPoint(value: 20, xAxisLabel: "M", pointLabel: "Monday" , colour: .purple), - BarChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday" , colour: .blue), - BarChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday", colour: Color(.cyan)), - BarChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday" , colour: .green), - BarChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday" , colour: .yellow), - BarChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday" , colour: .orange), - BarChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday" , colour: .red) - ], - legendTitle: "Data") - - return BarChartData(dataSets : data, - metadata : ChartMetadata(title : "Test Data", - subtitle: "A weeks worth"), - barStyle : BarStyle(barWidth : 0.5, - colourFrom: .dataPoints, - colour : .blue), - chartStyle: BarChartStyle(infoBoxPlacement : .floating, - xAxisLabelPosition : .bottom, - xAxisLabelsFrom : .dataPoint, - yAxisLabelPosition : .leading, - yAxisNumberOfLabels: 5)) - } - ``` + */ public final class BarChartData: CTBarChartDataProtocol { // MARK: Properties @@ -90,17 +62,14 @@ public final class BarChartData: CTBarChartDataProtocol { public final func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { - case .dataPoint: - - HStack(spacing: 0) { + case .dataPoint(let angle): + + HStack(alignment: .top, spacing: 0) { ForEach(dataSets.dataPoints) { data in Spacer() .frame(minWidth: 0, maxWidth: 500) - Text(data.wrappedXAxisLabel) - .font(.caption) + YAxisDataPointCell(chartData: self, label: data.wrappedXAxisLabel, rotationAngle: angle) .foregroundColor(self.chartStyle.xAxisLabelColour) - .lineLimit(1) - .minimumScaleFactor(0.5) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data.wrappedXAxisLabel)")) Spacer() @@ -108,6 +77,7 @@ public final class BarChartData: CTBarChartDataProtocol { } } + case .chartData: if let labelArray = self.xAxisLabels { @@ -119,7 +89,6 @@ public final class BarChartData: CTBarChartDataProtocol { .font(.caption) .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) - .minimumScaleFactor(0.5) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) Spacer() @@ -155,7 +124,45 @@ public final class BarChartData: CTBarChartDataProtocol { return nil } - public typealias Set = BarDataSet - public typealias DataPoint = BarChartDataPoint - public typealias CTStyle = BarChartStyle + public typealias Set = BarDataSet + public typealias DataPoint = BarChartDataPoint + public typealias CTStyle = BarChartStyle +} + +struct YAxisDataPointCell: View where ChartData: CTLineBarChartDataProtocol { + + @ObservedObject var chartData : ChartData + + private let label : String + private let rotationAngle : Angle + + init(chartData: ChartData, label: String, rotationAngle : Angle) { + self.chartData = chartData + self.label = label + self.rotationAngle = rotationAngle + } + + @State private var width: CGFloat = 0 + + 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) + } + + } } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift index 6579d98a..0c1a3147 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -11,69 +11,6 @@ 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. - - # Example - ``` - static func makeData() -> GroupedBarChartData { - - 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: 50, 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: 20, xAxisLabel: "2.1", pointLabel: "Two One" , group: Group.one.data), - MultiBarChartDataPoint(value: 60, 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: 30, xAxisLabel: "3.1", pointLabel: "Three One" , group: Group.one.data), - MultiBarChartDataPoint(value: 70, 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: 90, xAxisLabel: "3.4", pointLabel: "Three Four" , group: Group.four.data) - ]), - - MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", pointLabel: "Four One" , group: Group.one.data), - MultiBarChartDataPoint(value: 80, 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 GroupedBarChartData(dataSets : data, - groups : groups, - metadata : ChartMetadata(title: "Hello", subtitle: "Bob"), - chartStyle : BarChartStyle(infoBoxPlacement: .floating, - xAxisLabelsFrom : .dataPoint)) - } ``` */ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { @@ -130,22 +67,19 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { // MARK: Labels public final func getXAxisLabels() -> some View { - Group { + VStack { switch self.chartStyle.xAxisLabelsFrom { - case .dataPoint: + 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) - Text(data.wrappedXAxisLabel) - .font(.caption) + YAxisDataPointCell(chartData: self, label: data.group.title, rotationAngle: angle) .foregroundColor(self.chartStyle.xAxisLabelColour) - .lineLimit(1) - .minimumScaleFactor(0.5) - .accessibilityLabel(Text("XAxisLabel")) - .accessibilityValue(Text("\(data.wrappedXAxisLabel)")) + .accessibilityLabel(Text("X Axis Label")) + .accessibilityValue(Text("\(data.group.title)")) Spacer() .frame(minWidth: 0, maxWidth: 500) } @@ -165,8 +99,7 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { .font(.caption) .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) - .minimumScaleFactor(0.5) - .accessibilityLabel(Text("XAxisLabel")) + .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) Spacer() .frame(minWidth: 0, maxWidth: 500) @@ -174,8 +107,24 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { } } } + 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() diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift index 4d8af3ca..33304fa6 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift @@ -68,17 +68,14 @@ public final class RangedBarChartData: CTRangedBarChartDataProtocol { public final func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { - case .dataPoint: + case .dataPoint(let angle): HStack(spacing: 0) { ForEach(dataSets.dataPoints) { data in Spacer() .frame(minWidth: 0, maxWidth: 500) - Text(data.wrappedXAxisLabel) - .font(.caption) + YAxisDataPointCell(chartData: self, label: data.wrappedXAxisLabel, rotationAngle: angle) .foregroundColor(self.chartStyle.xAxisLabelColour) - .lineLimit(1) - .minimumScaleFactor(0.5) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data.wrappedXAxisLabel)")) Spacer() @@ -99,7 +96,6 @@ public final class RangedBarChartData: CTRangedBarChartDataProtocol { .font(.caption) .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) - .minimumScaleFactor(0.5) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) if data != labelArray[labelArray.count-1] { diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index b1ba9aa9..b6743296 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -125,16 +125,14 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { public final func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { - case .dataPoint: + case .dataPoint(let angle): HStack(spacing: 0) { ForEach(groups) { group in Spacer() .frame(minWidth: 0, maxWidth: 500) - Text(group.title) - .font(.caption) + YAxisDataPointCell(chartData: self, label: group.title, rotationAngle: angle) .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) - .minimumScaleFactor(0.5) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(group.title)")) @@ -152,7 +150,6 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { .font(.caption) .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) - .minimumScaleFactor(0.5) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) Spacer() diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift index 6c22c1be..fede66cb 100644 --- a/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift +++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift @@ -14,10 +14,10 @@ import SwiftUI ``` let data = MultiBarDataSets(dataSets: [ MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", pointLabel: "One One", group: GroupingData(title: "One", colour: .blue)) + MultiBarChartDataPoint(value: 10, group: GroupingData(title: "One", colour: .blue)) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 20, xAxisLabel: "2.1", pointLabel: "Two One", group: GroupingData(title: "One", colour: .blue)) + MultiBarChartDataPoint(value: 20, group: GroupingData(title: "One", colour: .blue)) ]) ]) ``` @@ -39,19 +39,23 @@ public struct MultiBarDataSets: CTMultiDataSetProtocol { # Example ``` MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", pointLabel: "One One", group: GroupingData(title: "One", colour: .blue)), - MultiBarChartDataPoint(value: 50, xAxisLabel: "1.2", pointLabel: "One Two", group: GroupingData(title: "Two", colour: .red)) + 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 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]) { - self.dataPoints = dataPoints + public init(dataPoints: [MultiBarChartDataPoint], + setTitle : String = "" + ) { + self.dataPoints = dataPoints + self.setTitle = setTitle } public typealias ID = UUID diff --git a/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift index c657dfad..95dd6b07 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift @@ -16,38 +16,38 @@ import SwiftUI ``` BarChartDataPoint(value: 90, xAxisLabel: "T", - pointLabel: "Tuesday", - colour: .blue) + description: "Tuesday", + colour: ColourStyle(colour: .blue)) ``` */ public struct BarChartDataPoint: CTStandardBarDataPoint { public let id = UUID() - public var value : Double - public var xAxisLabel : String? - public var pointDescription : String? - public var date : Date? - public var fillColour : ColourStyle + public var value : Double + public var xAxisLabel : String? + public var description: String? + public var date : Date? + public var colour : ColourStyle // 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. - /// - pointLabel: A longer label that can be shown on touch input. + /// - description: A longer label that can be shown on touch input. /// - date: Date of the data point if any data based calculations are required. - /// - fillColour: Colour styling for the fill. + /// - colour: Colour styling for the fill. public init(value : Double, xAxisLabel : String? = nil, - pointLabel : String? = nil, + description : String? = nil, date : Date? = nil, - fillColour : ColourStyle = ColourStyle(colour: .red) + colour : ColourStyle = ColourStyle(colour: .red) ) { - self.value = value - self.xAxisLabel = xAxisLabel - self.pointDescription = pointLabel - self.date = date - self.fillColour = fillColour + 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 index c0e0b4fd..1fd01c34 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift @@ -13,36 +13,31 @@ import SwiftUI # Example ``` MultiBarChartDataPoint(value: 10, - xAxisLabel: "1.1", - pointLabel: "One One", - group: GroupingData(title: "One", colour: .blue)) + 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? - public var pointDescription : String? - public var date : Date? - - public var group : GroupingData + 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 init(value : Double, - xAxisLabel : String? = nil, - pointLabel : String? = nil, + description : String? = nil, date : Date? = nil, group : GroupingData ) { - self.value = value - self.xAxisLabel = xAxisLabel - self.pointDescription = pointLabel - self.date = date - - self.group = group - + 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 index 29435835..2636d5e2 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift @@ -9,37 +9,35 @@ import SwiftUI public struct RangedBarDataPoint : CTRangedBarDataPoint { - public let id = UUID() - - public var upperValue : Double - public var lowerValue : Double - public var xAxisLabel : String? - public var pointDescription : String? - public var date : Date? - public var fillColour : ColourStyle - + 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 /// Data model for a single data point with colour for use with a 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. - /// - pointLabel: A longer label that can be shown on touch input. + /// - description: A longer label that can be shown on touch input. /// - date: Date of the data point if any data based calculations are required. - /// - fillColour: Colour styling for the fill. + /// - colour: Colour styling for the fill. public init(lowerValue : Double, upperValue : Double, xAxisLabel : String? = nil, - pointLabel : String? = nil, + description : String? = nil, date : Date? = nil, - fillColour : ColourStyle = ColourStyle(colour: .red) + colour : ColourStyle = ColourStyle(colour: .red) ) { - self.upperValue = upperValue - self.lowerValue = lowerValue - self.xAxisLabel = xAxisLabel - self.pointDescription = pointLabel - self.date = date - self.fillColour = fillColour + 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 index a3a26f47..a457c8b1 100644 --- a/Sources/SwiftUICharts/BarChart/Models/GroupingData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/GroupingData.swift @@ -12,23 +12,23 @@ import SwiftUI # Example ``` - GroupingData(title: "One", fillColour: ColourStyle(colour: .blue)) + GroupingData(title: "One", colour: ColourStyle(colour: .blue)) ``` */ public struct GroupingData: CTBarColourProtocol, Hashable, Identifiable { - public let id : UUID = UUID() - public var title : String - public var fillColour: ColourStyle + public let id : UUID = UUID() + public var title : String + public var colour : ColourStyle /// Group with single colour /// - Parameters: /// - title: Title for legends - /// - fillColour: Colour styling for the bars. - public init(title : String, - fillColour: ColourStyle + /// - colour: Colour styling for the bars. + public init(title : String, + colour : ColourStyle ) { - self.title = title - self.fillColour = fillColour + self.title = title + self.colour = colour } } diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index fb73667f..572b25e1 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -47,7 +47,7 @@ public protocol CTRangedBarChartDataProtocol: CTBarChartDataProtocol {} */ public protocol CTBarChartStyle: CTLineBarChartStyle {} -public protocol CTBarStyle: Hashable { +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. @@ -55,7 +55,7 @@ public protocol CTBarStyle: Hashable { /// Where to get the colour data from. var colourFrom : ColourFrom { get set } /// Drawing style of the fill. - var fillColour : ColourStyle { get set } + var colour : ColourStyle { get set } } @@ -99,7 +99,7 @@ public protocol CTBarDataPointBaseProtocol: CTLineBarDataPointProtocol {} */ public protocol CTBarColourProtocol { /// Drawing style of the range fill. - var fillColour : ColourStyle { get set } + var colour : ColourStyle { get set } } /** diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift index df19756b..775de8a3 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocolsExtensions.swift @@ -59,8 +59,8 @@ extension CTBarChartDataProtocol where Self.Set.ID == UUID, internal func setupLegends() { switch self.barStyle.colourFrom { case .barStyle: - if self.barStyle.fillColour.colourType == .colour, - let colour = self.barStyle.fillColour.colour + if self.barStyle.colour.colourType == .colour, + let colour = self.barStyle.colour.colour { self.legends.append(LegendData(id : dataSets.id, legend : dataSets.legendTitle, @@ -68,8 +68,8 @@ extension CTBarChartDataProtocol where Self.Set.ID == UUID, strokeStyle: nil, prioity : 1, chartType : .bar)) - } else if self.barStyle.fillColour.colourType == .gradientColour, - let colours = self.barStyle.fillColour.colours + } else if self.barStyle.colour.colourType == .gradientColour, + let colours = self.barStyle.colour.colours { self.legends.append(LegendData(id : dataSets.id, legend : dataSets.legendTitle, @@ -79,8 +79,8 @@ extension CTBarChartDataProtocol where Self.Set.ID == UUID, strokeStyle: nil, prioity : 1, chartType : .bar)) - } else if self.barStyle.fillColour.colourType == .gradientStops, - let stops = self.barStyle.fillColour.stops + } else if self.barStyle.colour.colourType == .gradientStops, + let stops = self.barStyle.colour.stops { self.legends.append(LegendData(id : dataSets.id, legend : dataSets.legendTitle, @@ -95,9 +95,9 @@ extension CTBarChartDataProtocol where Self.Set.ID == UUID, for data in dataSets.dataPoints { - if data.fillColour.colourType == .colour, - let colour = data.fillColour.colour, - let legend = data.pointDescription + if data.colour.colourType == .colour, + let colour = data.colour.colour, + let legend = data.description { self.legends.append(LegendData(id : data.id, legend : legend, @@ -105,9 +105,9 @@ extension CTBarChartDataProtocol where Self.Set.ID == UUID, strokeStyle: nil, prioity : 1, chartType : .bar)) - } else if data.fillColour.colourType == .gradientColour, - let colours = data.fillColour.colours, - let legend = data.pointDescription + } else if data.colour.colourType == .gradientColour, + let colours = data.colour.colours, + let legend = data.description { self.legends.append(LegendData(id : data.id, legend : legend, @@ -117,9 +117,9 @@ extension CTBarChartDataProtocol where Self.Set.ID == UUID, strokeStyle: nil, prioity : 1, chartType : .bar)) - } else if data.fillColour.colourType == .gradientStops, - let stops = data.fillColour.stops, - let legend = data.pointDescription + } else if data.colour.colourType == .gradientStops, + let stops = data.colour.stops, + let legend = data.description { self.legends.append(LegendData(id : data.id, legend : legend, @@ -141,8 +141,8 @@ extension CTMultiBarChartDataProtocol { for group in self.groups { - if group.fillColour.colourType == .colour, - let colour = group.fillColour.colour + if group.colour.colourType == .colour, + let colour = group.colour.colour { self.legends.append(LegendData(id : group.id, legend : group.title, @@ -150,8 +150,8 @@ extension CTMultiBarChartDataProtocol { strokeStyle: nil, prioity : 1, chartType : .bar)) - } else if group.fillColour.colourType == .gradientColour, - let colours = group.fillColour.colours + } else if group.colour.colourType == .gradientColour, + let colours = group.colour.colours { self.legends.append(LegendData(id : group.id, legend : group.title, @@ -161,8 +161,8 @@ extension CTMultiBarChartDataProtocol { strokeStyle: nil, prioity : 1, chartType : .bar)) - } else if group.fillColour.colourType == .gradientStops, - let stops = group.fillColour.stops + } else if group.colour.colourType == .gradientStops, + let stops = group.colour.stops { self.legends.append(LegendData(id : group.id, legend : group.title, diff --git a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift index 2de9b3aa..4d9c2c75 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift @@ -37,11 +37,13 @@ public struct BarChartStyle: CTBarChartStyle { 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 @@ -80,12 +82,14 @@ public struct BarChartStyle: CTBarChartStyle { xAxisGridStyle : GridStyle = GridStyle(), xAxisLabelPosition : XAxisLabelPosistion = .bottom, xAxisLabelColour : Color = Color.primary, - xAxisLabelsFrom : LabelsFrom = .dataPoint, + 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, @@ -102,11 +106,13 @@ public struct BarChartStyle: CTBarChartStyle { 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 diff --git a/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift index 932de398..a0657d11 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarStyle.swift @@ -12,10 +12,10 @@ import SwiftUI # Example ``` - BarStyle(barWidth : 0.5, - cornerRadius : CornerRadius(top: 15), - colourFrom : .barStyle, - fillColour : ColourStyle(colour: .blue)) + BarStyle(barWidth : 0.5, + cornerRadius: CornerRadius(top: 15), + colourFrom : .barStyle, + colour : ColourStyle(colour: .blue)) ``` */ public struct BarStyle: CTBarStyle { @@ -23,8 +23,7 @@ public struct BarStyle: CTBarStyle { public var barWidth : CGFloat public var cornerRadius: CornerRadius public var colourFrom : ColourFrom - - public var fillColour : ColourStyle + public var colour : ColourStyle // MARK: - Single colour /// Bar Chart with single colour @@ -32,15 +31,15 @@ public struct BarStyle: CTBarStyle { /// - 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. - /// - fillColour: Single Colour + /// - colour: Single Colour public init(barWidth : CGFloat = 1, cornerRadius: CornerRadius = CornerRadius(top: 5.0, bottom: 0.0), colourFrom : ColourFrom = .barStyle, - fillColour : ColourStyle = ColourStyle(colour: .red) + colour : ColourStyle = ColourStyle(colour: .red) ) { self.barWidth = barWidth self.cornerRadius = cornerRadius self.colourFrom = colourFrom - self.fillColour = fillColour + self.colour = colour } } diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index 6a7f232c..1700cd83 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -59,8 +59,8 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD HStack(spacing: 0) { ForEach(dataSet.dataPoints) { dataPoint in - if dataPoint.group.fillColour.colourType == .colour, - let colour = dataPoint.group.fillColour.colour + if dataPoint.group.colour.colourType == .colour, + let colour = dataPoint.group.colour.colour { ColourBar(chartData : chartData, @@ -68,10 +68,10 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD colour : colour) .accessibilityLabel(Text("\(chartData.metadata.title)")) - } else if dataPoint.group.fillColour.colourType == .gradientColour, - let colours = dataPoint.group.fillColour.colours, - let startPoint = dataPoint.group.fillColour.startPoint, - let endPoint = dataPoint.group.fillColour.endPoint + } 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, @@ -81,10 +81,10 @@ public struct GroupedBarChart: View where ChartData: GroupedBarChartD endPoint : endPoint) .accessibilityLabel( Text("\(chartData.metadata.title)")) - } else if dataPoint.group.fillColour.colourType == .gradientStops, - let stops = dataPoint.group.fillColour.stops, - let startPoint = dataPoint.group.fillColour.startPoint, - let endPoint = dataPoint.group.fillColour.endPoint + } 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) diff --git a/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift index 1ee35ae4..a1091683 100644 --- a/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift @@ -46,8 +46,8 @@ internal struct RangedBarChartBarStyleSubView: View { var body: some View { - if chartData.barStyle.fillColour.colourType == .colour, - let colour = chartData.barStyle.fillColour.colour { + if chartData.barStyle.colour.colourType == .colour, + let colour = chartData.barStyle.colour.colour { ForEach(chartData.dataSets.dataPoints) { dataPoint in GeometryReader { geo in RangedBarChartColourCell(chartData : chartData, @@ -56,10 +56,10 @@ internal struct RangedBarChartBarStyleSubView: View { barSize : geo.frame(in: .local)) } } - } else if chartData.barStyle.fillColour.colourType == .gradientColour, - let colours = chartData.barStyle.fillColour.colours, - let startPoint = chartData.barStyle.fillColour.startPoint, - let endPoint = chartData.barStyle.fillColour.endPoint { + } 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, @@ -70,10 +70,10 @@ internal struct RangedBarChartBarStyleSubView: View { barSize : geo.frame(in: .local)) } } - } else if chartData.barStyle.fillColour.colourType == .gradientStops, - let stops = chartData.barStyle.fillColour.stops, - let startPoint = chartData.barStyle.fillColour.startPoint, - let endPoint = chartData.barStyle.fillColour.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) @@ -103,18 +103,18 @@ internal struct RangedBarChartDataPointSubView: View { ForEach(chartData.dataSets.dataPoints) { dataPoint in GeometryReader { geo in - if dataPoint.fillColour.colourType == .colour, - let colour = dataPoint.fillColour.colour { + 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.fillColour.colourType == .gradientColour, - let colours = dataPoint.fillColour.colours, - let startPoint = dataPoint.fillColour.startPoint, - let endPoint = dataPoint.fillColour.endPoint { + } 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, @@ -122,10 +122,10 @@ internal struct RangedBarChartDataPointSubView: View { startPoint: startPoint, endPoint : endPoint, barSize : geo.frame(in: .local)) - } else if dataPoint.fillColour.colourType == .gradientStops, - let stops = dataPoint.fillColour.stops, - let startPoint = dataPoint.fillColour.startPoint, - let endPoint = dataPoint.fillColour.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) RangedBarChartStopsCell(chartData : chartData, diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift index a85feb87..fe6761ec 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift @@ -20,8 +20,8 @@ internal struct BarChartBarStyleSubView: View { } internal var body: some View { - if chartData.barStyle.fillColour.colourType == .colour, - let colour = chartData.barStyle.fillColour.colour + if chartData.barStyle.colour.colourType == .colour, + let colour = chartData.barStyle.colour.colour { ForEach(chartData.dataSets.dataPoints) { dataPoint in @@ -30,10 +30,10 @@ internal struct BarChartBarStyleSubView: View { colour : colour) } - } else if chartData.barStyle.fillColour.colourType == .gradientColour, - let colours = chartData.barStyle.fillColour.colours, - let startPoint = chartData.barStyle.fillColour.startPoint, - let endPoint = chartData.barStyle.fillColour.endPoint + } 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 @@ -44,10 +44,10 @@ internal struct BarChartBarStyleSubView: View { endPoint : endPoint) } - } else if chartData.barStyle.fillColour.colourType == .gradientStops, - let stops = chartData.barStyle.fillColour.stops, - let startPoint = chartData.barStyle.fillColour.startPoint, - let endPoint = chartData.barStyle.fillColour.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) @@ -80,18 +80,18 @@ internal struct BarChartDataPointSubView: View { ForEach(chartData.dataSets.dataPoints) { dataPoint in - if dataPoint.fillColour.colourType == .colour, - let colour = dataPoint.fillColour.colour + if dataPoint.colour.colourType == .colour, + let colour = dataPoint.colour.colour { ColourBar(chartData : chartData, dataPoint : dataPoint, colour : colour) - } else if dataPoint.fillColour.colourType == .gradientColour, - let colours = dataPoint.fillColour.colours, - let startPoint = dataPoint.fillColour.startPoint, - let endPoint = dataPoint.fillColour.endPoint + } else if dataPoint.colour.colourType == .gradientColour, + let colours = dataPoint.colour.colours, + let startPoint = dataPoint.colour.startPoint, + let endPoint = dataPoint.colour.endPoint { GradientColoursBar(chartData : chartData, @@ -100,10 +100,10 @@ internal struct BarChartDataPointSubView: View { startPoint : startPoint, endPoint : endPoint) - } else if dataPoint.fillColour.colourType == .gradientStops, - let stops = dataPoint.fillColour.stops, - let startPoint = dataPoint.fillColour.startPoint, - let endPoint = dataPoint.fillColour.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) diff --git a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift index 1b5b7e6f..e40ee124 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift @@ -172,8 +172,8 @@ internal struct StackElementSubView: View { VStack(spacing: 0) { ForEach(dataSet.dataPoints.reversed()) { dataPoint in - if dataPoint.group.fillColour.colourType == .colour, - let colour = dataPoint.group.fillColour.colour + if dataPoint.group.colour.colourType == .colour, + let colour = dataPoint.group.colour.colour { ColourPartBar(colour, getHeight(height : geo.size.height, @@ -181,10 +181,10 @@ internal struct StackElementSubView: View { dataPoint : dataPoint)) .accessibilityValue(dataPoint.getCellAccessibilityValue(specifier: specifier)) - } else if dataPoint.group.fillColour.colourType == .gradientColour, - let colours = dataPoint.group.fillColour.colours, - let startPoint = dataPoint.group.fillColour.startPoint, - let endPoint = dataPoint.group.fillColour.endPoint + } 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, @@ -192,10 +192,10 @@ internal struct StackElementSubView: View { dataPoint : dataPoint)) .accessibilityValue(dataPoint.getCellAccessibilityValue(specifier: specifier)) - } else if dataPoint.group.fillColour.colourType == .gradientStops, - let stops = dataPoint.group.fillColour.stops, - let startPoint = dataPoint.group.fillColour.startPoint, - let endPoint = dataPoint.group.fillColour.endPoint + } 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) diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index 145b1c9f..8e20f5d7 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -93,19 +93,14 @@ public final class LineChartData: CTLineChartDataProtocol { public final func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { - case .dataPoint: + case .dataPoint(let angle): HStack(spacing: 0) { ForEach(dataSets.dataPoints) { data in - if let label = data.xAxisLabel { - Text(label) - .font(.caption) - .foregroundColor(self.chartStyle.xAxisLabelColour) - .lineLimit(1) - .minimumScaleFactor(0.5) - .accessibilityLabel(Text("X Axis Label")) - .accessibilityValue(Text("\(data.wrappedXAxisLabel)")) - } + 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) @@ -122,7 +117,6 @@ public final class LineChartData: CTLineChartDataProtocol { .font(.caption) .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) - .minimumScaleFactor(0.5) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) if data != labelArray[labelArray.count - 1] { diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index 3bfaaccc..4321a079 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -66,9 +66,7 @@ public final class MultiLineChartData: CTLineChartDataProtocol { public final var noDataText : Text public final var chartType : (chartType: ChartType, dataSetType: DataSetType) - - internal final var isFilled : Bool = false - + // MARK: Initializers /// Initialises a Multi Line Chart. /// @@ -99,19 +97,14 @@ public final class MultiLineChartData: CTLineChartDataProtocol { public final func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { - case .dataPoint: + case .dataPoint(let angle): HStack(spacing: 0) { ForEach(dataSets.dataSets[0].dataPoints) { data in - if let label = data.xAxisLabel { - Text(label) - .font(.caption) - .foregroundColor(self.chartStyle.xAxisLabelColour) - .lineLimit(1) - .minimumScaleFactor(0.5) - .accessibilityLabel(Text("X Axis Label")) - .accessibilityValue(Text("\(data.wrappedXAxisLabel)")) - } + 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) @@ -129,7 +122,6 @@ public final class MultiLineChartData: CTLineChartDataProtocol { .font(.caption) .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) - .minimumScaleFactor(0.5) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) if data != labelArray[labelArray.count - 1] { @@ -151,7 +143,7 @@ public final class MultiLineChartData: CTLineChartDataProtocol { minValue : self.minValue, range : self.range, animation : self.chartStyle.globalAnimation, - isFilled : self.isFilled) + isFilled : false) } } diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift index ff1dc2ca..dd03fd08 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift @@ -22,9 +22,7 @@ public final class RangedLineChartData: CTLineChartDataProtocol { public var noDataText : Text public var chartType : (chartType: ChartType, dataSetType: DataSetType) - - internal var isFilled : Bool = false - + // MARK: Initializer /// Initialises a Single Line Chart. /// @@ -62,19 +60,14 @@ public final class RangedLineChartData: CTLineChartDataProtocol { public func getXAxisLabels() -> some View { Group { switch self.chartStyle.xAxisLabelsFrom { - case .dataPoint: + case .dataPoint(let angle): HStack(spacing: 0) { ForEach(dataSets.dataPoints) { data in - if let label = data.xAxisLabel { - Text(label) - .font(.caption) - .foregroundColor(self.chartStyle.xAxisLabelColour) - .lineLimit(1) - .minimumScaleFactor(0.5) - .accessibilityLabel(Text("X Axis Label")) - .accessibilityValue(Text("\(data.wrappedXAxisLabel)")) - } + 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) @@ -91,7 +84,6 @@ public final class RangedLineChartData: CTLineChartDataProtocol { .font(.caption) .foregroundColor(self.chartStyle.xAxisLabelColour) .lineLimit(1) - .minimumScaleFactor(0.5) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) if data != labelArray[labelArray.count - 1] { @@ -112,7 +104,7 @@ public final class RangedLineChartData: CTLineChartDataProtocol { minValue : self.minValue, range : self.range, animation : self.chartStyle.globalAnimation, - isFilled : self.isFilled) + isFilled : false) } public func getTouchInteraction(touchLocation: CGPoint, chartSize: CGRect) -> some View { diff --git a/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift b/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift index 12aa8d08..31ed758e 100644 --- a/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift @@ -14,39 +14,32 @@ import SwiftUI ``` LineChartDataPoint(value : 20, xAxisLabel : "M", - pointLabel : "Monday", + description: "Monday", date : Date()) ``` */ public struct LineChartDataPoint: CTStandardLineDataPoint { - public let id : UUID = UUID() - - public var value : Double - public var xAxisLabel : String? - public var pointDescription : String? - public var date : Date? - - var testlabel : String - + public let id : UUID = UUID() + public var value : Double + public var xAxisLabel : String? + public var description : String? + public var date : Date? + /// 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. - /// - pointLabel: A longer label that can be shown on touch input. + /// - 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, - pointLabel : String? = nil, - date : Date? = nil, - - testlabel : String = "" + xAxisLabel : String? = nil, + description : String? = nil, + date : Date? = nil ) { - self.value = value - self.xAxisLabel = xAxisLabel - self.pointDescription = pointLabel - self.date = date - - self.testlabel = testlabel + 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 index 6dfa0fc7..89bf9493 100644 --- a/Sources/SwiftUICharts/LineChart/Models/DataPoints/RangedLineChartDataPoint.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataPoints/RangedLineChartDataPoint.swift @@ -16,20 +16,18 @@ import SwiftUI upperValue: 20, lowerValue: 0, xAxisLabel: "M", - pointLabel: "Monday") + description: "Monday") ``` */ public struct RangedLineChartDataPoint: CTRangedLineDataPoint { - public let id : UUID = UUID() - - public var value : Double - public var xAxisLabel : String? - public var pointDescription : String? - public var date : Date? - - public var upperValue : Double - public var lowerValue : Double + 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? /// Data model for a single data point with colour for use with a ranged line chart. /// - Parameters: @@ -37,20 +35,20 @@ public struct RangedLineChartDataPoint: CTRangedLineDataPoint { /// - 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. - /// - pointLabel: A longer label that can be shown on touch input. + /// - 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, - pointLabel : String? = nil, + description : String? = nil, date : Date? = nil ) { - self.value = value - self.upperValue = upperValue - self.lowerValue = lowerValue - self.xAxisLabel = xAxisLabel - self.pointDescription = pointLabel - self.date = date + 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/Style/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift index 18a74e86..15fc9d5c 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift @@ -34,18 +34,20 @@ public struct LineChartStyle: CTLineChartStyle { public var infoBoxPlacement : InfoBoxPlacement public var infoBoxValueColour : Color - public var infoBoxDescriptionColour : Color + public var infoBoxDescriptionColour: Color 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 @@ -83,12 +85,14 @@ public struct LineChartStyle: CTLineChartStyle { xAxisGridStyle : GridStyle = GridStyle(), xAxisLabelPosition : XAxisLabelPosistion = .bottom, xAxisLabelColour : Color = Color.primary, - xAxisLabelsFrom : LabelsFrom = .dataPoint, + 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, @@ -105,11 +109,13 @@ public struct LineChartStyle: CTLineChartStyle { 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 diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift index df597cf5..3422160d 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift @@ -15,11 +15,11 @@ import SwiftUI # Example ``` static func makeData() -> DoughnutChartData { - let data = PieDataSet(dataPoints: [PieChartDataPoint(value: 7, pointDescription: "One", colour: .blue), - PieChartDataPoint(value: 2, pointDescription: "Two", colour: .red), - PieChartDataPoint(value: 9, pointDescription: "Three", colour: .purple), - PieChartDataPoint(value: 6, pointDescription: "Four", colour: .green), - PieChartDataPoint(value: 4, pointDescription: "Five", colour: .orange)], + let data = PieDataSet(dataPoints: [PieChartDataPoint(value: 7, description: "One", colour: .blue), + PieChartDataPoint(value: 2, description: "Two", colour: .red), + PieChartDataPoint(value: 9, description: "Three", colour: .purple), + PieChartDataPoint(value: 6, description: "Four", colour: .green), + PieChartDataPoint(value: 4, description: "Five", colour: .orange)], legendTitle: "Data") return DoughnutChartData(dataSets: data, diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift index a37ffc06..8593f618 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift @@ -16,11 +16,11 @@ import SwiftUI ``` static func makeData() -> PieChartData { let data = PieDataSet(dataPoints: [ - PieChartDataPoint(value: 7, pointDescription: "One", colour: .blue), - PieChartDataPoint(value: 2, pointDescription: "Two", colour: .red), - PieChartDataPoint(value: 9, pointDescription: "Three", colour: .purple), - PieChartDataPoint(value: 6, pointDescription: "Four", colour: .green), - PieChartDataPoint(value: 4, pointDescription: "Five", colour: .orange)], + PieChartDataPoint(value: 7, description: "One", colour: .blue), + PieChartDataPoint(value: 2, description: "Two", colour: .red), + PieChartDataPoint(value: 9, description: "Three", colour: .purple), + PieChartDataPoint(value: 6, description: "Four", colour: .green), + PieChartDataPoint(value: 4, description: "Five", colour: .orange)], legendTitle: "Data") return PieChartData(dataSets: data, diff --git a/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift b/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift index 0a0d279e..f7ac88f2 100644 --- a/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift +++ b/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift @@ -13,36 +13,34 @@ import SwiftUI # Example ``` PieChartDataPoint(value: 7, - pointDescription: "One", + description: "One", colour: .blue), ``` */ public struct PieChartDataPoint: CTPieDataPoint { - public var id : UUID = UUID() - public var value : Double - public var pointDescription : String? - public var date : Date? - - public var colour : Color - + 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 /// Data model for a single data point for a pie chart. /// - Parameters: /// - value: Value of the data point - /// - pointLabel: A longer label that can be shown on touch input. + /// - 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, - pointDescription: String? = nil, - date : Date? = nil, - colour : Color = Color.red + public init(value : Double, + description : String? = nil, + date : Date? = nil, + colour : Color = Color.red ) { - self.value = value - self.pointDescription = pointDescription - self.date = date - self.colour = colour + self.value = value + self.description = description + self.date = date + self.colour = colour } } diff --git a/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift b/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift index 662c8d37..ab5ff4d5 100644 --- a/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift +++ b/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift @@ -13,11 +13,11 @@ import SwiftUI # Example ``` PieDataSet(dataPoints: [ - PieChartDataPoint(value: 7, pointDescription: "One", colour: .blue), - PieChartDataPoint(value: 2, pointDescription: "Two", colour: .red), - PieChartDataPoint(value: 9, pointDescription: "Three", colour: .purple), - PieChartDataPoint(value: 6, pointDescription: "Four", colour: .green), - PieChartDataPoint(value: 4, pointDescription: "Five", colour: .orange)], + PieChartDataPoint(value: 7, description: "One", colour: .blue), + PieChartDataPoint(value: 2, description: "Two", colour: .red), + PieChartDataPoint(value: 9, description: "Three", colour: .purple), + PieChartDataPoint(value: 6, description: "Four", colour: .green), + PieChartDataPoint(value: 4, description: "Five", colour: .orange)], legendTitle: "Data") ``` */ diff --git a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift index 6930b8bb..6e952577 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Protocols/PieChartProtocolsExtentions.swift @@ -59,7 +59,7 @@ extension CTPieDoughnutChartDataProtocol where Self.Set.DataPoint.ID == UUID, Self.Set.DataPoint: CTPieDataPoint { internal func setupLegends() { for data in dataSets.dataPoints { - if let legend = data.pointDescription { + if let legend = data.description { self.legends.append(LegendData(id : data.id, legend : legend, colour : ColourStyle(colour: data.colour), diff --git a/Sources/SwiftUICharts/Shared/API.swift b/Sources/SwiftUICharts/Shared/API.swift index bdae5eac..feb94417 100644 --- a/Sources/SwiftUICharts/Shared/API.swift +++ b/Sources/SwiftUICharts/Shared/API.swift @@ -37,6 +37,26 @@ public struct InfoDescription : View where T: CTChartData { } } +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 { public func getLegend(textColor: Color) -> some View { Group { diff --git a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift index 940798ac..90b86651 100644 --- a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift @@ -82,7 +82,7 @@ public enum InfoBoxPlacement { case suffix(of: String) // After value ``` */ -public enum Unit { +public enum TouchUnit { /// No units case none /// Before value diff --git a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift index 939af996..aaa811ad 100644 --- a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift @@ -72,5 +72,5 @@ public struct InfoViewData { /** Option to display units before or after values. */ - var touchUnit: Unit = .none + var touchUnit: TouchUnit = .none } diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 4bafc8a7..db7caa52 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -28,9 +28,15 @@ public protocol CTChartData: ObservableObject, Identifiable { /// A type representing the chart style. -- `CTChartStyle` associatedtype CTStyle: CTChartStyle - /// A type representing opaque View + /// A type representing a view for the results of the touch interaction. associatedtype Touch: View + /// A type representing a View to get the touch value. + associatedtype InfoValue: View + + /// A type representing a View to get the touch description. + associatedtype InfoDesc: View + var id: ID { get } @@ -124,10 +130,15 @@ public protocol CTChartData: ObservableObject, Identifiable { func getPointLocation(dataSet: SetPoint, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? - associatedtype InfoValue : View - associatedtype InfoDesc : View - + + /** + Returns a Text View containing the data points value. + */ func infoValue(info: DataPoint) -> InfoValue + + /** + Returns a Text View containing the data points description. + */ func infoDescription(info: DataPoint) -> InfoDesc } @@ -136,7 +147,29 @@ public protocol CTChartData: ObservableObject, Identifiable { Main protocol to set conformace for types of Data Sets. */ public protocol CTDataSetProtocol: Hashable, Identifiable { - var id : ID { get } + 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 + } /** @@ -144,12 +177,12 @@ public protocol CTDataSetProtocol: Hashable, Identifiable { */ public protocol CTSingleDataSetProtocol: CTDataSetProtocol { /// A type representing a data point. -- `CTChartDataPoint` - associatedtype DataPoint : CTDataPointBaseProtocol + associatedtype DataPoint: CTDataPointBaseProtocol /** Array of data points. */ - var dataPoints : [DataPoint] { get set } + var dataPoints: [DataPoint] { get set } } @@ -158,12 +191,12 @@ public protocol CTSingleDataSetProtocol: CTDataSetProtocol { */ public protocol CTMultiDataSetProtocol: CTDataSetProtocol { /// A type representing a single data set -- `SingleDataSet` - associatedtype DataSet : CTSingleDataSetProtocol + associatedtype DataSet: CTSingleDataSetProtocol /** Array of single data sets. */ - var dataSets : [DataSet] { get set } + var dataSets: [DataSet] { get set } } @@ -175,7 +208,7 @@ public protocol CTMultiDataSetProtocol: CTDataSetProtocol { Protocol to set base configuration for data points. */ public protocol CTDataPointBaseProtocol: Hashable, Identifiable { - var id : ID { get } + var id: ID { get } /** A label that can be displayed on touch input @@ -183,13 +216,19 @@ public protocol CTDataPointBaseProtocol: Hashable, Identifiable { It can be displayed in a floating box that tracks the users input location or placed in the header. */ - var pointDescription : String? { get set } + var description: String? { get set } /** Date can be used for optionally performing additional calculations. */ - var date : Date? { get set } + var date: Date? { get set } + /** + Gets the relevant value(s) from the data point. + + - Parameter specifier: Specifier + - Returns: Value as a string. + */ func valueAsString(specifier: String) -> String } @@ -201,7 +240,7 @@ public protocol CTStandardDataPointProtocol: CTDataPointBaseProtocol { /** Value of the data point */ - var value : Double { get set } + var value: Double { get set } } /** @@ -210,10 +249,10 @@ public protocol CTStandardDataPointProtocol: CTDataPointBaseProtocol { */ public protocol CTRangeDataPointProtocol: CTDataPointBaseProtocol { /// Value of the upper range of the data point. - var upperValue : Double { get set } + var upperValue: Double { get set } /// Value of the lower range of the data point. - var lowerValue : Double { get set } + var lowerValue: Double { get set } } @@ -229,17 +268,17 @@ public protocol CTChartStyle { /** Placement of the information box that appears on touch input. */ - var infoBoxPlacement : InfoBoxPlacement { get set } + var infoBoxPlacement: InfoBoxPlacement { get set } /** Colour of the value part of the touch info. */ - var infoBoxValueColour : Color { get set } + var infoBoxValueColour: Color { get set } /** Colour of the description part of the touch info. */ - var infoBoxDescriptionColour : Color { get set } + var infoBoxDescriptionColour: Color { get set } /** Global control of animations. diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift index fb00ece4..4db6b80d 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift @@ -49,13 +49,13 @@ extension CTChartData { } // MARK: - Data Set -extension CTSingleDataSetProtocol where Self.DataPoint: CTStandardDataPointProtocol { +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. */ - func maxValue() -> Double { + public func maxValue() -> Double { return self.dataPoints.max { $0.value < $1.value }?.value ?? 0 } @@ -64,7 +64,7 @@ extension CTSingleDataSetProtocol where Self.DataPoint: CTStandardDataPointProto - Parameter dataSet: Target data set. - Returns: Lowest value in data set. */ - func minValue() -> Double { + public func minValue() -> Double { return self.dataPoints.min { $0.value < $1.value }?.value ?? 0 } @@ -73,11 +73,41 @@ extension CTSingleDataSetProtocol where Self.DataPoint: CTStandardDataPointProto - Parameter dataSet: Target data set. - Returns: Average of values in data set. */ - func average() -> Double { + 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 { @@ -86,7 +116,7 @@ extension CTMultiDataSetProtocol where Self.DataSet.DataPoint: CTStandardDataPoi - Parameter dataSet: Target data sets. - Returns: Highest value in data sets. */ - func maxValue() -> Double { + public func maxValue() -> Double { var setHolder : [Double] = [] for set in self.dataSets { setHolder.append(set.dataPoints.max { $0.value < $1.value }?.value ?? 0) @@ -99,7 +129,7 @@ extension CTMultiDataSetProtocol where Self.DataSet.DataPoint: CTStandardDataPoi - Parameter dataSet: Target data sets. - Returns: Lowest value in data sets. */ - func minValue() -> Double { + public func minValue() -> Double { var setHolder : [Double] = [] for set in dataSets { setHolder.append(set.dataPoints.min { $0.value < $1.value }?.value ?? 0) @@ -112,7 +142,7 @@ extension CTMultiDataSetProtocol where Self.DataSet.DataPoint: CTStandardDataPoi - Parameter dataSet: Target data sets. - Returns: Average of values in data sets. */ - func average() -> Double { + public func average() -> Double { var setHolder : [Double] = [] for set in dataSets { let sum = set.dataPoints.reduce(0) { $0 + $1.value } @@ -132,7 +162,7 @@ extension CTDataPointBaseProtocol { extension CTDataPointBaseProtocol { public var wrappedDescription : String { - self.pointDescription ?? "" + self.description ?? "" } } extension CTStandardDataPointProtocol { diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index f3dd0061..815b7132 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -17,7 +17,7 @@ internal struct TouchOverlay: ViewModifier where T: CTChartData { internal init(chartData : T, specifier : String, - unit : Unit + unit : TouchUnit ) { self.chartData = chartData self.chartData.infoView.touchSpecifier = specifier @@ -77,7 +77,7 @@ extension View { */ public func touchOverlay(chartData: T, specifier: String = "%.0f", - unit : Unit = .none + unit : TouchUnit = .none ) -> some View { self.modifier(TouchOverlay(chartData: chartData, specifier: specifier, @@ -92,7 +92,7 @@ extension View { */ public func touchOverlay(chartData: T, specifier: String = "%.0f", - unit : Unit + unit : TouchUnit ) -> some View { self.modifier(EmptyModifier()) } diff --git a/Sources/SwiftUICharts/Shared/Views/LegendView.swift b/Sources/SwiftUICharts/Shared/Views/LegendView.swift index 31efa25d..434172fc 100644 --- a/Sources/SwiftUICharts/Shared/Views/LegendView.swift +++ b/Sources/SwiftUICharts/Shared/Views/LegendView.swift @@ -51,7 +51,7 @@ internal struct LegendView: View where T: CTChartData { } } 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.fillColour + return chartData.infoView.isTouchCurrent && legend.colour == datapoint.group.colour } else { return false } diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index 075b245e..842f1bb0 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -14,20 +14,20 @@ internal struct TouchOverlayBox: View { @ObservedObject var chartData: T - @Binding private var boxFrame : CGRect + @Binding private var boxFrame: CGRect - internal init(chartData : T, - boxFrame : Binding + internal init(chartData: T, + boxFrame : Binding ) { - self.chartData = chartData - self._boxFrame = boxFrame + self.chartData = chartData + self._boxFrame = boxFrame } - + internal var body: some View { HStack { ForEach(chartData.infoView.touchOverlayInfo, id: \.id) { point in - + chartData.infoValue(info: point) .font(.subheadline) .foregroundColor(chartData.chartStyle.infoBoxValueColour) @@ -38,7 +38,7 @@ internal struct TouchOverlayBox: View { } } - + .padding(.all, 8) .background( GeometryReader { geo in diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift b/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift index 56d5c838..e2f1f6b4 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Extras/LineAndBarEnums.swift @@ -5,7 +5,7 @@ // Created by Will Dale on 08/02/2021. // -import Foundation +import SwiftUI // MARK: - XAxisLabels /** @@ -33,7 +33,7 @@ public enum XAxisLabelPosistion { */ public enum LabelsFrom { /// ChartData --> DataPoint --> xAxisLabel - case dataPoint + case dataPoint(rotation: Angle) /// ChartData --> xAxisLabels case chartData } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift index 5e98b30f..54448e93 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift @@ -5,13 +5,16 @@ // Created by Will Dale on 03/01/2021. // -import Foundation +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 + + var xAxisTitleHeight : CGFloat = 0 + 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/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index 59e593d7..3b6cb77f 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -13,28 +13,28 @@ import SwiftUI */ public protocol CTLineBarChartDataProtocol: CTChartData where CTStyle: CTLineBarChartStyle { - /// A type representing opaque View + /// 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 } + var range: Double { get } /** Returns the lowest value in the data set or data sets. */ - var minValue : Double { get } + var minValue: Double { get } /** Returns the highest value in the data set or data sets */ - var maxValue : Double { get } + var maxValue: Double { get } /** Returns the average value from the data set or data sets. */ - var average : Double { get } + var average : Double { get } /** Array of strings for the labels on the X Axis instead of the labels in the data points. @@ -49,7 +49,6 @@ public protocol CTLineBarChartDataProtocol: CTChartData where CTStyle: CTLineBar */ var viewData: ChartViewData { get set } - /** Labels to display on the Y axis @@ -64,7 +63,7 @@ public protocol CTLineBarChartDataProtocol: CTChartData where CTStyle: CTLineBar Displays a view for the labels on the X Axis. */ func getXAxisLabels() -> XLabels - + } @@ -80,13 +79,15 @@ public protocol MarkerType {} public protocol CTLineBarChartStyle: CTChartStyle { /// A type representing touch overlay marker type. -- `MarkerType` - associatedtype Mark : 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. */ @@ -107,6 +108,8 @@ public protocol CTLineBarChartStyle: CTChartStyle { */ var xAxisLabelsFrom: LabelsFrom { get set } + var xAxisTitle: String? { get set } + /** Style of the horizontal lines breaking up the chart. @@ -128,6 +131,8 @@ public protocol CTLineBarChartStyle: CTChartStyle { */ var yAxisNumberOfLabels: Int { get set } + var yAxisTitle: String? { get set } + /** Where to start drawing the line chart from. Zero, data set minium or custom. */ @@ -136,7 +141,7 @@ public protocol CTLineBarChartStyle: CTChartStyle { /** Where to finish drawing the chart from. Data set maximum or custom. */ - var topLine : Topline { get set } + var topLine: Topline { get set } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift index 76784852..f755c8ea 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift @@ -7,9 +7,8 @@ import Foundation -// MARK: - Single Data Set -extension CTLineBarChartDataProtocol where Set: CTSingleDataSetProtocol, - Set.DataPoint: CTStandardDataPointProtocol & CTnotRanged { +// MARK: - Data Set +extension CTLineBarChartDataProtocol { public var range : Double { var _lowestValue : Double @@ -58,104 +57,7 @@ extension CTLineBarChartDataProtocol where Set: CTSingleDataSetProtocol, return self.dataSets.average() } } -extension CTLineBarChartDataProtocol where Set: CTSingleDataSetProtocol, - Set.DataPoint: CTRangeDataPointProtocol & CTisRanged { - public var range : Double { - - var _lowestValue : Double - var _highestValue : Double - - switch self.chartStyle.baseline { - case .minimumValue: - _lowestValue = dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0 - case .minimumWithMaximum(of: let value): - _lowestValue = min(dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0, value) - case .zero: - _lowestValue = 0 - } - - switch self.chartStyle.topLine { - case .maximumValue: - _highestValue = dataSets.dataPoints.max(by: { $0.upperValue < $1.upperValue })?.upperValue ?? 0 - case .maximum(of: let value): - _highestValue = max(dataSets.dataPoints.max(by: { $0.upperValue < $1.upperValue })?.upperValue ?? 0, value) - } - - return (_highestValue - _lowestValue) + 0.001 - } - - public var minValue : Double { - switch self.chartStyle.baseline { - case .minimumValue: - return dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0 - case .minimumWithMaximum(of: let value): - return min(dataSets.dataPoints.min(by: { $0.lowerValue < $1.lowerValue })?.lowerValue ?? 0, value) - case .zero: - return 0 - } - } - - public var maxValue : Double { - switch self.chartStyle.topLine { - case .maximumValue: - return dataSets.dataPoints.max(by: { $0.upperValue < $1.upperValue })?.upperValue ?? 0 - case .maximum(of: let value): - return max(dataSets.dataPoints.max(by: { $0.upperValue < $1.upperValue })?.upperValue ?? 0, value) - } - } -} - -// MARK: - Multi Data Set -extension CTLineBarChartDataProtocol where Set: CTMultiDataSetProtocol, - Self.Set.DataSet.DataPoint: CTStandardDataPointProtocol { - public var range : Double { - - 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 { - 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 { - 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 { diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift index 2db9425f..9a9892d8 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift @@ -22,23 +22,36 @@ internal struct XAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol internal func body(content: Content) -> some View { Group { switch chartData.chartStyle.xAxisLabelPosition { - case .top: + case .bottom: if chartData.isGreaterThanTwo() { VStack { - chartData.getXAxisLabels() content + chartData.getXAxisLabels() + axisTitle } } else { content } - case .bottom: + case .top: if chartData.isGreaterThanTwo() { VStack { - content + 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 { diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift index fde69a6b..0798873b 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift @@ -31,24 +31,33 @@ internal struct YAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol labelsAndBottom = chartData.viewData.hasXAxisLabels && chartData.chartStyle.xAxisLabelPosition == .bottom } - internal var textAsSpacer: some View { - Text("") - .font(.caption) - .lineLimit(1) - .minimumScaleFactor(0.5) + @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 + } + } } - internal var labels: some View { + private var labels: some View { VStack { - if labelsAndTop { - textAsSpacer - } ForEach((0...chartData.chartStyle.yAxisNumberOfLabels-1).reversed(), id: \.self) { i in Text("\(labelsArray[i], specifier: specifier)") .font(.caption) .foregroundColor(chartData.chartStyle.yAxisLabelColour) .lineLimit(1) - .minimumScaleFactor(0.5) .accessibilityLabel(Text("Y Axis Label")) .accessibilityValue(Text("\(labelsArray[i], specifier: specifier)")) if i != 0 { @@ -56,9 +65,8 @@ internal struct YAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol .frame(minHeight: 0, maxHeight: 500) } } - if labelsAndBottom { - textAsSpacer - } + 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) } @@ -69,10 +77,8 @@ internal struct YAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol .foregroundColor(Color.clear) .onAppear { chartData.infoView.yAxisLabelWidth = geo.frame(in: .local).size.width + self.height = geo.frame(in: .local).height } -// .onChange(of: geo.frame(in: .local).size.width) { value in -// chartData.infoView.yAxisLabelWidth = value -// } } ) } @@ -83,6 +89,7 @@ internal struct YAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol switch chartData.chartStyle.yAxisLabelPosition { case .leading: HStack(spacing: 0) { + axisTitle labels content } @@ -90,6 +97,7 @@ internal struct YAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol HStack(spacing: 0) { content labels + axisTitle } } } else { content } diff --git a/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift index 4fa04aa7..c58ec3d1 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift @@ -13,13 +13,13 @@ final class GroupedBarChartTests: XCTestCase { var data : GroupingData { switch self { case .one: - return GroupingData(title: "One" , fillColour: ColourStyle(colour: .blue)) + return GroupingData(title: "One" , colour: ColourStyle(colour: .blue)) case .two: - return GroupingData(title: "Two" , fillColour: ColourStyle(colour: .red)) + return GroupingData(title: "Two" , colour: ColourStyle(colour: .red)) case .three: - return GroupingData(title: "Three", fillColour: ColourStyle(colour: .yellow)) + return GroupingData(title: "Three", colour: ColourStyle(colour: .yellow)) case .four: - return GroupingData(title: "Four" , fillColour: ColourStyle(colour: .green)) + return GroupingData(title: "Four" , colour: ColourStyle(colour: .green)) } } } @@ -28,31 +28,31 @@ final class GroupedBarChartTests: XCTestCase { let data = MultiBarDataSets(dataSets: [ MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", pointLabel: "One One" , group: Group.one.data), - MultiBarChartDataPoint(value: 50, 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) + MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", description: "One One" , group: Group.one.data), + MultiBarChartDataPoint(value: 50, xAxisLabel: "1.2", description: "One Two" , group: Group.two.data), + MultiBarChartDataPoint(value: 30, xAxisLabel: "1.3", description: "One Three" , group: Group.three.data), + MultiBarChartDataPoint(value: 40, xAxisLabel: "1.4", description: "One Four" , group: Group.four.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 20, xAxisLabel: "2.1", pointLabel: "Two One" , group: Group.one.data), - MultiBarChartDataPoint(value: 60, 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) + MultiBarChartDataPoint(value: 20, xAxisLabel: "2.1", description: "Two One" , group: Group.one.data), + MultiBarChartDataPoint(value: 60, xAxisLabel: "2.2", description: "Two Two" , group: Group.two.data), + MultiBarChartDataPoint(value: 40, xAxisLabel: "2.3", description: "Two Three" , group: Group.three.data), + MultiBarChartDataPoint(value: 60, xAxisLabel: "2.3", description: "Two Four" , group: Group.four.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 30, xAxisLabel: "3.1", pointLabel: "Three One" , group: Group.one.data), - MultiBarChartDataPoint(value: 70, 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: 90, xAxisLabel: "3.4", pointLabel: "Three Four" , group: Group.four.data) + MultiBarChartDataPoint(value: 30, xAxisLabel: "3.1", description: "Three One" , group: Group.one.data), + MultiBarChartDataPoint(value: 70, xAxisLabel: "3.2", description: "Three Two" , group: Group.two.data), + MultiBarChartDataPoint(value: 30, xAxisLabel: "3.3", description: "Three Three", group: Group.three.data), + MultiBarChartDataPoint(value: 90, xAxisLabel: "3.4", description: "Three Four" , group: Group.four.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", pointLabel: "Four One" , group: Group.one.data), - MultiBarChartDataPoint(value: 80, 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) + MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", description: "Four One" , group: Group.one.data), + MultiBarChartDataPoint(value: 80, xAxisLabel: "4.2", description: "Four Two" , group: Group.two.data), + MultiBarChartDataPoint(value: 20, xAxisLabel: "4.3", description: "Four Three" , group: Group.three.data), + MultiBarChartDataPoint(value: 50, xAxisLabel: "4.3", description: "Four Four" , group: Group.four.data) ]) ]) @@ -84,20 +84,20 @@ final class GroupedBarChartTests: XCTestCase { func testGroupedBarIsGreaterThanTwoFalse() { let data = MultiBarDataSets(dataSets: [ MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", pointLabel: "One One" , group: Group.one.data) + MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", description: "One One" , group: Group.one.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 20, xAxisLabel: "2.1", pointLabel: "Two One" , group: Group.one.data) + MultiBarChartDataPoint(value: 20, xAxisLabel: "2.1", description: "Two One" , group: Group.one.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 30, xAxisLabel: "3.1", pointLabel: "Three One", group: Group.one.data) + MultiBarChartDataPoint(value: 30, xAxisLabel: "3.1", description: "Three One", group: Group.one.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", pointLabel: "Four One" , group: Group.one.data) + MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", description: "Four One" , group: Group.one.data) ]) ]) let chartData = GroupedBarChartData(dataSets: data, groups: groups) @@ -109,11 +109,27 @@ final class GroupedBarChartTests: XCTestCase { let chartData = GroupedBarChartData(dataSets: data, groups: groups, chartStyle: BarChartStyle(yAxisNumberOfLabels: 3)) - XCTAssertEqual(chartData.getYLabels()[0], 0.00000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[1], 30.0000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[2], 60.0000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[3], 90.0000, accuracy: 0.01) - + 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() { diff --git a/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift index e31d6b56..48f3b6d3 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift @@ -13,13 +13,13 @@ final class StackedBarChartTests: XCTestCase { var data : GroupingData { switch self { case .one: - return GroupingData(title: "One" , fillColour: ColourStyle(colour: .blue)) + return GroupingData(title: "One" , colour: ColourStyle(colour: .blue)) case .two: - return GroupingData(title: "Two" , fillColour: ColourStyle(colour: .red)) + return GroupingData(title: "Two" , colour: ColourStyle(colour: .red)) case .three: - return GroupingData(title: "Three", fillColour: ColourStyle(colour: .yellow)) + return GroupingData(title: "Three", colour: ColourStyle(colour: .yellow)) case .four: - return GroupingData(title: "Four" , fillColour: ColourStyle(colour: .green)) + return GroupingData(title: "Four" , colour: ColourStyle(colour: .green)) } } } @@ -28,31 +28,31 @@ final class StackedBarChartTests: XCTestCase { let data = MultiBarDataSets(dataSets: [ MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", pointLabel: "One One" , group: Group.one.data), - MultiBarChartDataPoint(value: 50, 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) + MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", description: "One One" , group: Group.one.data), + MultiBarChartDataPoint(value: 50, xAxisLabel: "1.2", description: "One Two" , group: Group.two.data), + MultiBarChartDataPoint(value: 30, xAxisLabel: "1.3", description: "One Three" , group: Group.three.data), + MultiBarChartDataPoint(value: 40, xAxisLabel: "1.4", description: "One Four" , group: Group.four.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 20, xAxisLabel: "2.1", pointLabel: "Two One" , group: Group.one.data), - MultiBarChartDataPoint(value: 60, 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) + MultiBarChartDataPoint(value: 20, xAxisLabel: "2.1", description: "Two One" , group: Group.one.data), + MultiBarChartDataPoint(value: 60, xAxisLabel: "2.2", description: "Two Two" , group: Group.two.data), + MultiBarChartDataPoint(value: 40, xAxisLabel: "2.3", description: "Two Three" , group: Group.three.data), + MultiBarChartDataPoint(value: 60, xAxisLabel: "2.3", description: "Two Four" , group: Group.four.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 30, xAxisLabel: "3.1", pointLabel: "Three One" , group: Group.one.data), - MultiBarChartDataPoint(value: 70, 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: 90, xAxisLabel: "3.4", pointLabel: "Three Four" , group: Group.four.data) + MultiBarChartDataPoint(value: 30, xAxisLabel: "3.1", description: "Three One" , group: Group.one.data), + MultiBarChartDataPoint(value: 70, xAxisLabel: "3.2", description: "Three Two" , group: Group.two.data), + MultiBarChartDataPoint(value: 30, xAxisLabel: "3.3", description: "Three Three", group: Group.three.data), + MultiBarChartDataPoint(value: 90, xAxisLabel: "3.4", description: "Three Four" , group: Group.four.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", pointLabel: "Four One" , group: Group.one.data), - MultiBarChartDataPoint(value: 80, 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.4", pointLabel: "Four Four" , group: Group.four.data) + MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", description: "Four One" , group: Group.one.data), + MultiBarChartDataPoint(value: 80, xAxisLabel: "4.2", description: "Four Two" , group: Group.two.data), + MultiBarChartDataPoint(value: 20, xAxisLabel: "4.3", description: "Four Three" , group: Group.three.data), + MultiBarChartDataPoint(value: 50, xAxisLabel: "4.4", description: "Four Four" , group: Group.four.data) ]) ]) @@ -84,20 +84,19 @@ final class StackedBarChartTests: XCTestCase { func testStackedBarIsGreaterThanTwoFalse() { let data = MultiBarDataSets(dataSets: [ MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", pointLabel: "One One" , group: Group.one.data) + MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", description: "One One" , group: Group.one.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 20, xAxisLabel: "2.1", pointLabel: "Two One" , group: Group.one.data) + MultiBarChartDataPoint(value: 20, xAxisLabel: "2.1", description: "Two One" , group: Group.one.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 30, xAxisLabel: "3.1", pointLabel: "Three One", group: Group.one.data) - + MultiBarChartDataPoint(value: 30, xAxisLabel: "3.1", description: "Three One", group: Group.one.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", pointLabel: "Four One" , group: Group.one.data) + MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", description: "Four One" , group: Group.one.data) ]) ]) let chartData = StackedBarChartData(dataSets: data, groups: groups) @@ -109,10 +108,27 @@ final class StackedBarChartTests: XCTestCase { let chartData = StackedBarChartData(dataSets: data, groups: groups, chartStyle: BarChartStyle(yAxisNumberOfLabels: 3)) - XCTAssertEqual(chartData.getYLabels()[0], 0.00000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[1], 30.0000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[2], 60.0000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[3], 90.0000, accuracy: 0.01) + 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 diff --git a/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift b/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift index 1318bce5..6dde6a4e 100644 --- a/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift +++ b/Tests/SwiftUIChartsTests/LineCharts/LineChartTests.swift @@ -42,38 +42,33 @@ final class LineChartTests: XCTestCase { } // MARK: - Labels - func testLineGetYLabelsMinimumValue() { + func testLineGetYLabels() { let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints), - chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, - baseline: .minimumValue)) - XCTAssertEqual(chartData.getYLabels()[0], 10.0000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[1], 33.3333, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[2], 56.6666, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[3], 80.0000, accuracy: 0.01) + chartStyle: LineChartStyle(yAxisNumberOfLabels: 3)) - } - - func testLineGetYLabelsMinimumWithMax() { - let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints), - chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, - baseline: .minimumWithMaximum(of: 5))) - XCTAssertEqual(chartData.getYLabels()[0], 5.0000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[1], 30.000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[2], 55.000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[3], 80.0000, accuracy: 0.01) + 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) - } - - func testLineGetYLabelsZero() { - let chartData = LineChartData(dataSets: LineDataSet(dataPoints: dataPoints), - chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, - baseline: .zero)) - XCTAssertEqual(chartData.getYLabels()[0], 0.0000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[1], 26.666, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[2], 53.333, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[3], 80.0000, 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() { @@ -142,9 +137,7 @@ final class LineChartTests: XCTestCase { ("testLineIsGreaterThanTwoTrue", testLineIsGreaterThanTwoTrue), ("testLineIsGreaterThanTwoFalse", testLineIsGreaterThanTwoFalse), // Labels - ("testLineGetYLabelsMinimumValue", testLineGetYLabelsMinimumValue), - ("testLineGetYLabelsMinimumWithMax", testLineGetYLabelsMinimumWithMax), - ("testLineGetYLabelsZero", testLineGetYLabelsZero), + ("testLineGetYLabels", testLineGetYLabels), // Touch ("testLineGetDataPoint", testLineGetDataPoint), ("testLineGetPointLocation", testLineGetPointLocation), diff --git a/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift b/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift index d8003c25..57997dfb 100644 --- a/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift +++ b/Tests/SwiftUIChartsTests/LineCharts/MultiLineChartTest.swift @@ -57,41 +57,33 @@ final class MultiLineChartTest: XCTestCase { } // MARK: - Labels - func testMultiLineGetYLabelsMinimumValue() { + func testMultiLineGetYLabels() { let chartData = MultiLineChartData(dataSets: dataSet, - chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, - baseline: .minimumValue)) - - XCTAssertEqual(chartData.getYLabels()[0], 10.0000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[1], 40.0000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[2], 70.0000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[3], 100.0000, accuracy: 0.01) + chartStyle: LineChartStyle(yAxisNumberOfLabels: 3)) - } - - func testMultiLineGetYLabelsMinimumWithMax() { - let chartData = MultiLineChartData(dataSets: dataSet, - chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, - baseline: .minimumWithMaximum(of: 5))) - - XCTAssertEqual(chartData.getYLabels()[0], 5.0000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[1], 36.6666, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[2], 68.3333, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[3], 100.0000, accuracy: 0.01) + 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) - } - - func testMultiLineGetYLabelsZero() { - let chartData = MultiLineChartData(dataSets: dataSet, - chartStyle: LineChartStyle(yAxisNumberOfLabels: 3, - baseline: .zero)) - - XCTAssertEqual(chartData.getYLabels()[0], 0.0000, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[1], 33.3333, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[2], 66.6666, accuracy: 0.01) - XCTAssertEqual(chartData.getYLabels()[3], 100.0000, 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) @@ -189,9 +181,7 @@ final class MultiLineChartTest: XCTestCase { ("testMultiLineIsGreaterThanTwoTrue" , testMultiIsGreaterThanTwoTrue), ("testMultiLineIsGreaterThanTwoFalse", testMultiIsGreaterThanTwoFalse), // Labels - ("testMultiLineGetYLabelsMinimumValue" , testMultiLineGetYLabelsMinimumValue), - ("testMultiLineGetYLabelsMinimumWithMax", testMultiLineGetYLabelsMinimumWithMax), - ("testMultiLineGetYLabelsZero" , testMultiLineGetYLabelsZero), + ("testMultiLineGetYLabels" , testMultiLineGetYLabels), // Touch ("testMultiLineGetDataPoint", testMultiLineGetDataPoint), ("testMultiLineGetPointLocation", testMultiLineGetPointLocation), From 2542d8c0a88ec09366199c140d9176a7bdf86a50 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 07:55:03 +0000 Subject: [PATCH 129/152] Fix X axis labels bugs. --- .../Models/ChartData/BarChartData.swift | 52 +++++++++++++++---- .../ChartData/GroupedBarChartData.swift | 8 +-- .../Models/ChartData/RangedBarChartData.swift | 8 +-- .../ChartData/StackedBarChartData.swift | 8 +-- .../Models/ChartData/LineChartData.swift | 8 +-- .../Models/ChartData/MultiLineChartData.swift | 9 ++-- 6 files changed, 63 insertions(+), 30 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index b8e28c71..c275abbc 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -76,8 +76,7 @@ public final class BarChartData: CTBarChartDataProtocol { .frame(minWidth: 0, maxWidth: 500) } } - - + case .chartData: if let labelArray = self.xAxisLabels { @@ -85,10 +84,8 @@ public final class BarChartData: CTBarChartDataProtocol { ForEach(labelArray, id: \.self) { data in Spacer() .frame(minWidth: 0, maxWidth: 500) - Text(data) - .font(.caption) + YAxisChartDataCell(chartData: self, label: data) .foregroundColor(self.chartStyle.xAxisLabelColour) - .lineLimit(1) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) Spacer() @@ -109,7 +106,9 @@ public final class BarChartData: CTBarChartDataProtocol { let xSection : CGFloat = chartSize.width / CGFloat(dataSets.dataPoints.count) let index : Int = Int((touchLocation.x) / xSection) if index >= 0 && index < dataSets.dataPoints.count { - points.append(dataSets.dataPoints[index]) + var dataPoint = dataSets.dataPoints[index] + dataPoint.legendTag = dataSets.legendTitle + points.append(dataPoint) } self.infoView.touchOverlayInfo = points } @@ -129,14 +128,16 @@ public final class BarChartData: CTBarChartDataProtocol { public typealias CTStyle = BarChartStyle } -struct YAxisDataPointCell: View where ChartData: CTLineBarChartDataProtocol { + + +internal struct YAxisDataPointCell: View where ChartData: CTLineBarChartDataProtocol { @ObservedObject var chartData : ChartData private let label : String private let rotationAngle : Angle - init(chartData: ChartData, label: String, rotationAngle : Angle) { + internal init(chartData: ChartData, label: String, rotationAngle : Angle) { self.chartData = chartData self.label = label self.rotationAngle = rotationAngle @@ -144,7 +145,7 @@ struct YAxisDataPointCell: View where ChartData: CTLineBarChartDataPr @State private var width: CGFloat = 0 - var body: some View { + internal var body: some View { Text(label) .font(.caption) @@ -166,3 +167,36 @@ struct YAxisDataPointCell: View where ChartData: CTLineBarChartDataPr } } + +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 index 0c1a3147..8d2ec3ee 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/GroupedBarChartData.swift @@ -95,10 +95,8 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { ForEach(labelArray, id: \.self) { data in Spacer() .frame(minWidth: 0, maxWidth: 500) - Text(data) - .font(.caption) + YAxisChartDataCell(chartData: self, label: data) .foregroundColor(self.chartStyle.xAxisLabelColour) - .lineLimit(1) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) Spacer() @@ -149,7 +147,9 @@ public final class GroupedBarChartData: CTMultiBarChartDataProtocol { 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 { - points.append(dataSet.dataPoints[subIndex]) + var dataPoint = dataSet.dataPoints[subIndex] + dataPoint.legendTag = dataSet.setTitle + points.append(dataPoint) } } self.infoView.touchOverlayInfo = points diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift index 33304fa6..200ff348 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/RangedBarChartData.swift @@ -92,10 +92,8 @@ public final class RangedBarChartData: CTRangedBarChartDataProtocol { Spacer() .frame(minWidth: 0, maxWidth: 500) } - Text(data) - .font(.caption) + YAxisChartDataCell(chartData: self, label: data) .foregroundColor(self.chartStyle.xAxisLabelColour) - .lineLimit(1) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) if data != labelArray[labelArray.count-1] { @@ -118,7 +116,9 @@ public final class RangedBarChartData: CTRangedBarChartDataProtocol { let xSection : CGFloat = chartSize.width / CGFloat(dataSets.dataPoints.count) let index : Int = Int((touchLocation.x) / xSection) if index >= 0 && index < dataSets.dataPoints.count { - points.append(dataSets.dataPoints[index]) + var dataPoint = dataSets.dataPoints[index] + dataPoint.legendTag = dataSets.legendTitle + points.append(dataPoint) } self.infoView.touchOverlayInfo = points } diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index b6743296..fbde8d4c 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -146,10 +146,8 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { ForEach(labelArray, id: \.self) { data in Spacer() .frame(minWidth: 0, maxWidth: 500) - Text(data) - .font(.caption) + YAxisChartDataCell(chartData: self, label: data) .foregroundColor(self.chartStyle.xAxisLabelColour) - .lineLimit(1) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) Spacer() @@ -204,7 +202,9 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { if let index = yIndex?.offset { if index >= 0 && index < dataSet.dataPoints.count { - points.append(dataSet.dataPoints[index]) + var dataPoint = dataSet.dataPoints[index] + dataPoint.legendTag = dataSet.setTitle + points.append(dataPoint) } } } diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index 8e20f5d7..01b469c2 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -113,10 +113,8 @@ public final class LineChartData: CTLineChartDataProtocol { if let labelArray = self.xAxisLabels { HStack(spacing: 0) { ForEach(labelArray, id: \.self) { data in - Text(data) - .font(.caption) + YAxisChartDataCell(chartData: self, label: data) .foregroundColor(self.chartStyle.xAxisLabelColour) - .lineLimit(1) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) if data != labelArray[labelArray.count - 1] { @@ -177,7 +175,9 @@ extension LineChartData { 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 { - points.append(dataSets.dataPoints[index]) + 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 index 4321a079..5a50d27a 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -113,15 +113,12 @@ public final class MultiLineChartData: CTLineChartDataProtocol { } .padding(.horizontal, -4) - case .chartData: if let labelArray = self.xAxisLabels { HStack(spacing: 0) { ForEach(labelArray, id: \.self) { data in - Text(data) - .font(.caption) + YAxisChartDataCell(chartData: self, label: data) .foregroundColor(self.chartStyle.xAxisLabelColour) - .lineLimit(1) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) if data != labelArray[labelArray.count - 1] { @@ -201,7 +198,9 @@ extension MultiLineChartData { 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 { - points.append(dataSet.dataPoints[index]) + var dataPoint = dataSet.dataPoints[index] + dataPoint.legendTag = dataSet.legendTitle + points.append(dataPoint) } } self.infoView.touchOverlayInfo = points From e04e7ecd662623d121cd82166653f6889ec72ca4 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 07:57:11 +0000 Subject: [PATCH 130/152] Add link between data points and their data sets. --- .../Models/Datapoints/BarChartDataPoint.swift | 2 ++ .../Datapoints/MultiBarChartDataPoint.swift | 2 ++ .../Datapoints/RangedBarDataPoint.swift | 2 ++ .../DataPoints/LineChartDataPoint.swift | 2 ++ .../DataPoints/RangedLineChartDataPoint.swift | 2 ++ .../Models/DataPoints/PieChartDataPoint.swift | 2 ++ .../Models/Protocols/SharedProtocols.swift | 22 ++++--------------- 7 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift index 95dd6b07..16a3e1f0 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/BarChartDataPoint.swift @@ -30,6 +30,8 @@ public struct BarChartDataPoint: CTStandardBarDataPoint { 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: diff --git a/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift index 1fd01c34..aee09207 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/MultiBarChartDataPoint.swift @@ -27,6 +27,8 @@ public struct MultiBarChartDataPoint: CTMultiBarDataPoint { public var date : Date? public var group : GroupingData + public var legendTag : String = "" + public init(value : Double, description : String? = nil, date : Date? = nil, diff --git a/Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift index 2636d5e2..b4c3a0b1 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift @@ -16,6 +16,8 @@ public struct RangedBarDataPoint : CTRangedBarDataPoint { 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 bar chart. /// - Parameters: diff --git a/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift b/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift index 31ed758e..95e615d5 100644 --- a/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataPoints/LineChartDataPoint.swift @@ -25,6 +25,8 @@ public struct LineChartDataPoint: CTStandardLineDataPoint { 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: diff --git a/Sources/SwiftUICharts/LineChart/Models/DataPoints/RangedLineChartDataPoint.swift b/Sources/SwiftUICharts/LineChart/Models/DataPoints/RangedLineChartDataPoint.swift index 89bf9493..10578e18 100644 --- a/Sources/SwiftUICharts/LineChart/Models/DataPoints/RangedLineChartDataPoint.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataPoints/RangedLineChartDataPoint.swift @@ -28,6 +28,8 @@ public struct RangedLineChartDataPoint: CTRangedLineDataPoint { 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: diff --git a/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift b/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift index f7ac88f2..25b9bdf4 100644 --- a/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift +++ b/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift @@ -27,6 +27,8 @@ public struct PieChartDataPoint: CTPieDataPoint { 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 diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index db7caa52..43e3ca5c 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -31,13 +31,6 @@ public protocol CTChartData: ObservableObject, Identifiable { /// A type representing a view for the results of the touch interaction. associatedtype Touch: View - /// A type representing a View to get the touch value. - associatedtype InfoValue: View - - /// A type representing a View to get the touch description. - associatedtype InfoDesc: View - - var id: ID { get } /** @@ -129,17 +122,6 @@ public protocol CTChartData: ObservableObject, Identifiable { */ func getPointLocation(dataSet: SetPoint, touchLocation: CGPoint, chartSize: CGRect) -> CGPoint? - - - /** - Returns a Text View containing the data points value. - */ - func infoValue(info: DataPoint) -> InfoValue - - /** - Returns a Text View containing the data points description. - */ - func infoDescription(info: DataPoint) -> InfoDesc } // MARK: - Data Sets @@ -223,6 +205,8 @@ public protocol CTDataPointBaseProtocol: Hashable, Identifiable { */ var date: Date? { get set } + var legendTag : String { get set } + /** Gets the relevant value(s) from the data point. @@ -280,6 +264,8 @@ public protocol CTChartStyle { */ var infoBoxDescriptionColour: Color { get set } + var infoBoxBackgroundColour : Color { get set } + /** Global control of animations. From e3deb36ee34d2a3a3c3d841322e6c9e313be92f4 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 07:58:11 +0000 Subject: [PATCH 131/152] Add background colour for info box. --- .../SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift | 6 +++++- .../LineChart/Models/Style/LineChartStyle.swift | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift index 4d9c2c75..4bdaeb34 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift @@ -29,7 +29,8 @@ public struct BarChartStyle: CTBarChartStyle { public var infoBoxPlacement : InfoBoxPlacement public var infoBoxValueColour : Color - public var infoBoxDescriptionColour : Color + public var infoBoxDescriptionColour: Color + public var infoBoxBackgroundColour : Color public var markerType : BarMarkerType @@ -56,6 +57,7 @@ public struct BarChartStyle: CTBarChartStyle { /// - 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. /// /// - markerType: Where the marker lines come from to meet at a specified point. /// @@ -76,6 +78,7 @@ public struct BarChartStyle: CTBarChartStyle { public init(infoBoxPlacement : InfoBoxPlacement = .floating, infoBoxValueColour : Color = Color.primary, infoBoxDescriptionColour: Color = Color.primary, + infoBoxBackgroundColour : Color = Color.systemsBackground, markerType : BarMarkerType = .full, @@ -99,6 +102,7 @@ public struct BarChartStyle: CTBarChartStyle { self.infoBoxPlacement = infoBoxPlacement self.infoBoxValueColour = infoBoxValueColour self.infoBoxDescriptionColour = infoBoxDescriptionColour + self.infoBoxBackgroundColour = infoBoxBackgroundColour self.markerType = markerType diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift index 15fc9d5c..5c7b0f7d 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift @@ -35,6 +35,8 @@ public struct LineChartStyle: CTLineChartStyle { public var infoBoxPlacement : InfoBoxPlacement public var infoBoxValueColour : Color public var infoBoxDescriptionColour: Color + public var infoBoxBackgroundColour : Color + public var markerType : LineMarkerType public var xAxisGridStyle : GridStyle @@ -59,6 +61,7 @@ public struct LineChartStyle: CTLineChartStyle { /// - 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. /// /// - markerType: Where the marker lines come from to meet at a specified point. /// @@ -79,6 +82,7 @@ public struct LineChartStyle: CTLineChartStyle { public init(infoBoxPlacement : InfoBoxPlacement = .floating, infoBoxValueColour : Color = Color.primary, infoBoxDescriptionColour: Color = Color.primary, + infoBoxBackgroundColour : Color = Color.systemsBackground, markerType : LineMarkerType = .indicator(style: DotStyle()), @@ -102,6 +106,7 @@ public struct LineChartStyle: CTLineChartStyle { self.infoBoxPlacement = infoBoxPlacement self.infoBoxValueColour = infoBoxValueColour self.infoBoxDescriptionColour = infoBoxDescriptionColour + self.infoBoxBackgroundColour = infoBoxBackgroundColour self.markerType = markerType From dce30b402391be38c1fd91f7276cd3fc3cfbc6bc Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 07:58:43 +0000 Subject: [PATCH 132/152] Fix X axis label bugs. --- .../LineChart/Models/ChartData/RangedLineChartData.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift index dd03fd08..1af08529 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift @@ -80,10 +80,8 @@ public final class RangedLineChartData: CTLineChartDataProtocol { if let labelArray = self.xAxisLabels { HStack(spacing: 0) { ForEach(labelArray, id: \.self) { data in - Text(data) - .font(.caption) + YAxisChartDataCell(chartData: self, label: data) .foregroundColor(self.chartStyle.xAxisLabelColour) - .lineLimit(1) .accessibilityLabel(Text("X Axis Label")) .accessibilityValue(Text("\(data)")) if data != labelArray[labelArray.count - 1] { @@ -137,7 +135,9 @@ public final class RangedLineChartData: CTLineChartDataProtocol { 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 { - points.append(dataSets.dataPoints[index]) + var dataPoint = dataSets.dataPoints[index] + dataPoint.legendTag = dataSets.legendTitle + points.append(dataPoint) } self.infoView.touchOverlayInfo = points } From 77ea48fdc135696707d115111c5f349e1e04783e Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 07:59:30 +0000 Subject: [PATCH 133/152] Add link between data points and their data sets. --- .../PieChart/Models/ChartData/DoughnutChartData.swift | 4 +++- .../PieChart/Models/ChartData/PieChartData.swift | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift index 3422160d..2b779a6f 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift @@ -82,7 +82,9 @@ extension DoughnutChartData { 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 { - points.append(data) + var finalDataPoint = data + finalDataPoint.legendTag = dataSets.legendTitle + points.append(finalDataPoint) } self.infoView.touchOverlayInfo = points } diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift index 8593f618..a641bddf 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift @@ -83,7 +83,9 @@ extension PieChartData { 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 { - points.append(data) + var finalDataPoint = data + finalDataPoint.legendTag = dataSets.legendTitle + points.append(finalDataPoint) } self.infoView.touchOverlayInfo = points } From c6faa463223b52a970e60976485c886e7738a5a9 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 08:00:58 +0000 Subject: [PATCH 134/152] Add background colour to info box. --- .../PieChart/Models/Style/DoughnutChartStyle.swift | 5 +++++ .../SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift index 3330f5ac..dde6e9da 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift @@ -21,6 +21,7 @@ public struct DoughnutChartStyle: CTDoughnutChartStyle { public var infoBoxPlacement : InfoBoxPlacement public var infoBoxValueColour : Color public var infoBoxDescriptionColour : Color + public var infoBoxBackgroundColour : Color public var globalAnimation : Animation @@ -31,17 +32,21 @@ public struct DoughnutChartStyle: CTDoughnutChartStyle { /// - 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. /// - 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, + globalAnimation : Animation = Animation.linear(duration: 1), strokeWidth : CGFloat = 30 ) { self.infoBoxPlacement = infoBoxPlacement self.infoBoxValueColour = infoBoxValueColour self.infoBoxDescriptionColour = infoBoxDescriptionColour + self.infoBoxBackgroundColour = infoBoxBackgroundColour self.globalAnimation = globalAnimation self.strokeWidth = strokeWidth } diff --git a/Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift index 3acf5c60..c586bf27 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift @@ -21,7 +21,8 @@ public struct PieChartStyle: CTPieChartStyle { public var infoBoxPlacement : InfoBoxPlacement public var infoBoxValueColour : Color - public var infoBoxDescriptionColour : Color + public var infoBoxDescriptionColour : Color + public var infoBoxBackgroundColour : Color public var globalAnimation : Animation @@ -30,15 +31,18 @@ public struct PieChartStyle: CTPieChartStyle { /// - 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. /// - globalAnimation: Global control of animations. public init(infoBoxPlacement : InfoBoxPlacement = .floating, infoBoxValueColour : Color = Color.primary, infoBoxDescriptionColour: Color = Color.primary, + infoBoxBackgroundColour : Color = Color.systemsBackground, globalAnimation : Animation = Animation.linear(duration: 1) ) { self.infoBoxPlacement = infoBoxPlacement self.infoBoxValueColour = infoBoxValueColour self.infoBoxDescriptionColour = infoBoxDescriptionColour + self.infoBoxBackgroundColour = infoBoxBackgroundColour self.globalAnimation = globalAnimation } } From 0810a78a2096f2b40c4600ce6e1e0e71d8425dba Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 08:02:04 +0000 Subject: [PATCH 135/152] Add api options for info box. Tidy up. --- Sources/SwiftUICharts/Shared/API.swift | 41 ++++++++++++++++++- .../Protocols/SharedProtocolsExtensions.swift | 25 ++++++++++- .../Shared/ViewModifiers/HeaderBox.swift | 2 +- .../Shared/ViewModifiers/InfoBox.swift | 39 +++++++++++------- .../Shared/Views/TouchOverlayBox.swift | 33 ++++++++------- 5 files changed, 105 insertions(+), 35 deletions(-) diff --git a/Sources/SwiftUICharts/Shared/API.swift b/Sources/SwiftUICharts/Shared/API.swift index feb94417..c8fd88fd 100644 --- a/Sources/SwiftUICharts/Shared/API.swift +++ b/Sources/SwiftUICharts/Shared/API.swift @@ -17,7 +17,7 @@ public struct InfoValue : View where T: CTChartData { public var body: some View { ForEach(chartData.infoView.touchOverlayInfo, id: \.id) { point in - chartData.infoValue(info: point) + chartData.infoValueUnit(info: point) } } } @@ -184,6 +184,45 @@ extension LegendData { } } } + 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: diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift index 4db6b80d..3812c562 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift @@ -33,7 +33,7 @@ extension CTChartData { } extension CTChartData { - public func infoValue(info: DataPoint) -> some View { + public func infoValueUnit(info: DataPoint) -> some View { switch self.infoView.touchUnit { case .none: return Text("\(info.valueAsString(specifier: self.infoView.touchSpecifier))") @@ -43,9 +43,32 @@ extension CTChartData { return Text("\(info.valueAsString(specifier: self.infoView.touchSpecifier)) \(unit)") } } + public func infoValue(info: DataPoint) -> some View { + Text("\(info.valueAsString(specifier: self.infoView.touchSpecifier))") + } + public func infoUnit(info: DataPoint) -> 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)") + } + } public func infoDescription(info: DataPoint) -> some View { Text("\(info.wrappedDescription)") } + @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() + } + } } // MARK: - Data Set diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index 6c416612..38ac2084 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -35,7 +35,7 @@ internal struct HeaderBox: ViewModifier where T: CTChartData { if chartData.infoView.isTouchCurrent { ForEach(chartData.infoView.touchOverlayInfo, id: \.id) { point in - chartData.infoValue(info: point) + chartData.infoValueUnit(info: point) .font(.title3) .foregroundColor(chartData.chartStyle.infoBoxValueColour) diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift index 2b809ceb..70a91dd8 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift @@ -14,40 +14,49 @@ internal struct InfoBox: ViewModifier where T: CTChartData { @ObservedObject var chartData: T - @State private var boxFrame: CGRect = CGRect(x: 0, y: 0, width: 0, height: 50) + 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 { - VStack { + Group { switch chartData.chartStyle.infoBoxPlacement { case .floating: - floating + ZStack { + floating + content + } case .fixed: - fixed + VStack { + fixed + content + } case .header: EmptyView() } - content } } - var floating: some View { + private var floating: some View { TouchOverlayBox(chartData: chartData, boxFrame : $boxFrame) .position(x: setBoxLocationation(touchLocation: chartData.infoView.touchLocation.x, boxFrame : boxFrame, - chartSize : chartData.infoView.chartSize), - y: 15) - .frame(height: 40) + chartSize : chartData.infoView.chartSize) )//, + //y: 35) + .frame(height: 70) + .zIndex(1) } - var fixed: some View { - + private var fixed: some View { TouchOverlayBox(chartData: chartData, boxFrame : $boxFrame) - .frame(height: 40) - .padding(.horizontal, 6) - + .frame(height: 70) + .padding(.horizontal, 6) + .zIndex(1) } @@ -55,7 +64,7 @@ internal struct InfoBox: ViewModifier where T: CTChartData { /// - 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 { + private func setBoxLocationation(touchLocation: CGFloat, boxFrame: CGRect, chartSize: CGRect) -> CGFloat { var returnPoint : CGFloat = .zero diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index 842f1bb0..9a7ecd32 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -25,17 +25,21 @@ internal struct TouchOverlayBox: View { internal var body: some View { - HStack { + VStack(alignment: .leading, spacing: 0) { ForEach(chartData.infoView.touchOverlayInfo, id: \.id) { point in - chartData.infoValue(info: point) - .font(.subheadline) - .foregroundColor(chartData.chartStyle.infoBoxValueColour) - 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() } } @@ -43,19 +47,14 @@ internal struct TouchOverlayBox: View { .background( GeometryReader { geo in if chartData.infoView.isTouchCurrent { - Group { - RoundedRectangle(cornerRadius: 5.0, style: .continuous) - .fill(Color.systemsBackground) - } - .overlay( - Group { - RoundedRectangle(cornerRadius: 5.0) - .stroke(Color.primary, lineWidth: 1) + RoundedRectangle(cornerRadius: 5.0, style: .continuous) + .fill(chartData.chartStyle.infoBoxBackgroundColour) + .onAppear { + self.boxFrame = geo.frame(in: .local) + } + .onChange(of: geo.frame(in: .local)) { frame in + self.boxFrame = frame } - ) - .onChange(of: geo.frame(in: .local)) { frame in - self.boxFrame = frame - } } } ) From 40d83e0efa8d4530d8e88ed1cd970193dbd6b331 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 08:02:30 +0000 Subject: [PATCH 136/152] Squash a bug. --- .../SharedLineAndBar/ViewModifiers/YAxisLabels.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift index 0798873b..c1e8461b 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift @@ -79,6 +79,9 @@ internal struct YAxisLabels: ViewModifier where T: CTLineBarChartDataProtocol 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 + } } ) } From f877cea959c44e4cd2f91952423ee10b5c22fe1c Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 08:45:01 +0000 Subject: [PATCH 137/152] Refactor Touch interface. --- .../Shared/Extras/SharedEnums.swift | 8 +-- .../ViewModifiers/FloatingInfoBox.swift | 61 +++++++++++++++++++ .../Shared/ViewModifiers/HeaderBox.swift | 2 +- .../Shared/ViewModifiers/InfoBox.swift | 48 ++++++--------- .../Shared/ViewModifiers/TouchOverlay.swift | 2 +- .../LineAndBarProtocolsExtentions.swift | 22 ++++++- 6 files changed, 107 insertions(+), 36 deletions(-) create mode 100644 Sources/SwiftUICharts/Shared/ViewModifiers/FloatingInfoBox.swift diff --git a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift index 90b86651..25b1b44b 100644 --- a/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift +++ b/Sources/SwiftUICharts/Shared/Extras/SharedEnums.swift @@ -60,16 +60,16 @@ public enum ColourType { Placement of the data point information panel when touch overlay modifier is applied. ``` case floating // Follows input across the chart. - case fixed // Centered in view. + 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 - /// Centered in view. - case fixed - /// Fix in the Header box. Must have .headerBox(). + /// Display in the InfoBox. Must have .infoBox() + case infoBox(isStatic: Bool = false) + /// Display in the Header box. Must have .headerBox(). case header } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/FloatingInfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/FloatingInfoBox.swift new file mode 100644 index 00000000..7ba0a7ea --- /dev/null +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/FloatingInfoBox.swift @@ -0,0 +1,61 @@ +// +// FloatingInfoBox.swift +// +// +// Created by Will Dale on 12/03/2021. +// + +import SwiftUI + +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: 35) + .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 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 38ac2084..b960b8d4 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -64,7 +64,7 @@ internal struct HeaderBox: ViewModifier where T: CTChartData { titleBox content } - case .fixed: + case .infoBox: VStack(alignment: .leading) { titleBox content diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift index 70a91dd8..8ee1e6a4 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/InfoBox.swift @@ -24,17 +24,22 @@ internal struct InfoBox: ViewModifier where T: CTChartData { Group { switch chartData.chartStyle.infoBoxPlacement { case .floating: - ZStack { - floating - content - } - case .fixed: - VStack { - fixed - content + content + case .infoBox(let isStatic): + switch isStatic { + case true: + VStack { + fixed + content + } + case false: + VStack { + floating + content + } } case .header: - EmptyView() + content } } } @@ -42,11 +47,12 @@ internal struct InfoBox: ViewModifier where T: CTChartData { private var floating: some View { TouchOverlayBox(chartData: chartData, boxFrame : $boxFrame) - .position(x: setBoxLocationation(touchLocation: chartData.infoView.touchLocation.x, + .position(x: chartData.setBoxLocationation(touchLocation: chartData.infoView.touchLocation.x, boxFrame : boxFrame, - chartSize : chartData.infoView.chartSize) )//, - //y: 35) + chartSize : chartData.infoView.chartSize), + y: 35) .frame(height: 70) + .padding(.horizontal, 6) .zIndex(1) } @@ -60,23 +66,7 @@ internal struct InfoBox: ViewModifier where T: CTChartData { } - /// Sets the 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. - private 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 + chartData.infoView.yAxisLabelWidth - } + } extension View { diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 815b7132..7869e869 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -23,7 +23,7 @@ internal struct TouchOverlay: ViewModifier where T: CTChartData { self.chartData.infoView.touchSpecifier = specifier self.chartData.infoView.touchUnit = unit } - + internal func body(content: Content) -> some View { Group { if chartData.isGreaterThanTwo() { diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift index f755c8ea..003b4476 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift @@ -5,7 +5,7 @@ // Created by Will Dale on 13/02/2021. // -import Foundation +import SwiftUI // MARK: - Data Set extension CTLineBarChartDataProtocol { @@ -73,3 +73,23 @@ extension CTLineBarChartDataProtocol { return labels } } + +extension CTChartData { + /// Sets the 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 + } +} From 79ff73fdbcfa31dc817e29a109a540a2ef750c49 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 08:55:27 +0000 Subject: [PATCH 138/152] Refactor. --- .../Protocols/SharedProtocolsExtensions.swift | 19 ++++++++++++++++++ .../LineAndBarProtocolsExtentions.swift | 20 ------------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift index 3812c562..4d9addd4 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift @@ -70,6 +70,25 @@ extension CTChartData { } } } +extension CTChartData { + /// Sets the 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 { diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift index 003b4476..aa049290 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift @@ -73,23 +73,3 @@ extension CTLineBarChartDataProtocol { return labels } } - -extension CTChartData { - /// Sets the 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 - } -} From 49c1a794ffdc97abcfcbb6b42562b60cf0d636c3 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 09:34:49 +0000 Subject: [PATCH 139/152] Refactor. --- .../BarChart/Views/RangedBarChart.swift | 278 ++---------------- .../Views/SubViews/BarChartSubViews.swift | 113 ++++++- .../BarChart/Views/SubViews/Bars.swift | 147 +++++++++ 3 files changed, 286 insertions(+), 252 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift index a1091683..219af14e 100644 --- a/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift @@ -7,6 +7,34 @@ 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) + .yAxisPOI(chartData: data) + .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 @@ -35,253 +63,3 @@ public struct RangedBarChart: View where ChartData: RangedBarChartDat } else { CustomNoDataView(chartData: chartData) } } } - -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)) - } - } - } - } -} - -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)) - } - } - } - } -} - -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/BarChart/Views/SubViews/BarChartSubViews.swift b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift index fe6761ec..808c03d4 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/BarChartSubViews.swift @@ -7,7 +7,8 @@ import SwiftUI -// MARK: - Bar Style +// MARK: - Standard +// MARK: Bar Style /** Bar segment where the colour information comes from chart style. */ @@ -64,7 +65,7 @@ internal struct BarChartBarStyleSubView: View { } } -// MARK: - DataPoints +// MARK: DataPoints /** Bar segment where the colour information comes from datapoints. */ @@ -122,3 +123,111 @@ internal struct BarChartDataPointSubView: View { } } } + +// 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 index e40ee124..e074c160 100644 --- a/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift +++ b/Sources/SwiftUICharts/BarChart/Views/SubViews/Bars.swift @@ -315,3 +315,150 @@ internal struct GradientStopsPartBar: View { .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)) + } +} From b1928482c51fc697ccb185e6ecfef385f9252b86 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 09:35:19 +0000 Subject: [PATCH 140/152] Fix Equatable. --- .../Models/DataPoints/PieChartDataPoint.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift b/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift index 25b9bdf4..5634e4f9 100644 --- a/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift +++ b/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift @@ -46,3 +46,16 @@ public struct PieChartDataPoint: CTPieDataPoint { 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) + } +} From ab105b61e6e394bcd09c570165c06facb3bd43dd Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 11:19:15 +0000 Subject: [PATCH 141/152] Update documentation. --- .../Models/ChartData/BarChartData.swift | 2 - .../Models/DataSet/MultiBarDataSets.swift | 12 ---- .../Models/DataSet/RangedBarDataSet.swift | 7 ++- .../Datapoints/RangedBarDataPoint.swift | 13 ++-- .../Models/Protocols/BarChartProtocols.swift | 5 +- .../BarChart/Models/Style/BarChartStyle.swift | 13 +--- .../BarChart/Views/BarChart.swift | 26 ++++---- .../BarChart/Views/GroupedBarChart.swift | 26 ++++---- .../BarChart/Views/RangedBarChart.swift | 13 ++-- .../BarChart/Views/StackedBarChart.swift | 26 ++++---- .../Models/ChartData/LineChartData.swift | 32 ---------- .../Models/ChartData/MultiLineChartData.swift | 39 ------------ .../ChartData/RangedLineChartData.swift | 7 ++- .../Models/DataSet/LineDataSet.swift | 16 ----- .../Models/DataSet/MultiLineDataSet.swift | 30 ---------- .../Models/DataSet/RangedLineDataSet.swift | 21 +------ .../Models/Protocols/LineChartProtocols.swift | 10 ++++ .../LineChartProtocolsExtensions.swift | 4 +- .../Models/Style/LineChartStyle.swift | 19 +----- .../LineChart/Models/Style/LineStyle.swift | 12 +--- .../Models/Style/RangedLineStyle.swift | 7 --- .../ViewModifiers/PointMarkers.swift | 16 ++--- .../LineChart/Views/FilledLineChart.swift | 1 + .../LineChart/Views/LineChartView.swift | 1 + .../LineChart/Views/MultiLineChart.swift | 1 + .../LineChart/Views/RangedLineChart.swift | 37 ++++++++++++ .../Models/ChartData/DoughnutChartData.swift | 19 +----- .../Models/ChartData/PieChartData.swift | 19 +----- .../Models/DataPoints/PieChartDataPoint.swift | 7 --- .../PieChart/Models/DataSets/PieDataSet.swift | 11 ---- .../Models/Style/DoughnutChartStyle.swift | 6 -- .../PieChart/Models/Style/PieChartStyle.swift | 7 --- .../PieChart/Views/DoughnutChart.swift | 3 +- .../PieChart/Views/PieChart.swift | 1 + Sources/SwiftUICharts/Shared/API.swift | 21 +++++++ .../Shared/Models/ChartMetadata.swift | 5 -- .../Shared/Models/ColourStyle.swift | 8 +-- .../Shared/Models/InfoViewData.swift | 12 ++-- .../Models/Protocols/SharedProtocols.swift | 1 + .../Protocols/SharedProtocolsExtensions.swift | 59 +++++++++++++++++-- .../Shared/Types/GradientStop.swift | 2 +- .../ViewModifiers/FloatingInfoBox.swift | 5 ++ .../Shared/ViewModifiers/HeaderBox.swift | 4 +- .../Shared/ViewModifiers/TouchOverlay.swift | 7 ++- .../Models/ChartViewData.swift | 16 ++++- .../SharedLineAndBar/Models/GridStyle.swift | 9 --- .../Protocols/LineAndBarProtocols.swift | 12 +++- .../ViewModifiers/XAxisGrid.swift | 6 +- .../ViewModifiers/XAxisLabels.swift | 7 ++- .../ViewModifiers/YAxisGrid.swift | 6 +- .../ViewModifiers/YAxisLabels.swift | 4 +- .../ViewModifiers/YAxisPOI.swift | 42 +++++++------ .../BarCharts/GroupedBarChartTests.swift | 40 ++++++------- .../BarCharts/StackedBarChartTests.swift | 40 ++++++------- 54 files changed, 352 insertions(+), 423 deletions(-) diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift index c275abbc..904b38e9 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/BarChartData.swift @@ -9,8 +9,6 @@ import SwiftUI /** Data for drawing and styling a standard Bar Chart. - - */ public final class BarChartData: CTBarChartDataProtocol { // MARK: Properties diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift index fede66cb..b776fff6 100644 --- a/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift +++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/MultiBarDataSets.swift @@ -9,18 +9,6 @@ import SwiftUI /** Main data set for a multi part bar charts. - - # Example - ``` - let data = MultiBarDataSets(dataSets: [ - MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 10, group: GroupingData(title: "One", colour: .blue)) - ]), - MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 20, group: GroupingData(title: "One", colour: .blue)) - ]) - ]) - ``` */ public struct MultiBarDataSets: CTMultiDataSetProtocol { diff --git a/Sources/SwiftUICharts/BarChart/Models/DataSet/RangedBarDataSet.swift b/Sources/SwiftUICharts/BarChart/Models/DataSet/RangedBarDataSet.swift index 01204a75..725c7c94 100644 --- a/Sources/SwiftUICharts/BarChart/Models/DataSet/RangedBarDataSet.swift +++ b/Sources/SwiftUICharts/BarChart/Models/DataSet/RangedBarDataSet.swift @@ -7,16 +7,19 @@ 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 standard Bar Charts. + /// Initialises a new data set for ranged bar chart. /// - Parameters: /// - dataPoints: Array of elements. - /// - legendTitle: label for the data in legend. + /// - legendTitle: Label for the data in legend. public init(dataPoints : [RangedBarDataPoint], legendTitle : String = "" ) { diff --git a/Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift b/Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift index b4c3a0b1..b23d843a 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Datapoints/RangedBarDataPoint.swift @@ -7,7 +7,10 @@ import SwiftUI -public struct RangedBarDataPoint : CTRangedBarDataPoint { +/** + Data for a single ranged bar chart data point. + */ +public struct RangedBarDataPoint: CTRangedBarDataPoint { public let id : UUID = UUID() public var upperValue : Double @@ -19,7 +22,7 @@ public struct RangedBarDataPoint : CTRangedBarDataPoint { public var legendTag : String = "" - /// Data model for a single data point with colour for use with a bar chart. + /// 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. @@ -29,9 +32,9 @@ public struct RangedBarDataPoint : CTRangedBarDataPoint { /// - colour: Colour styling for the fill. public init(lowerValue : Double, upperValue : Double, - xAxisLabel : String? = nil, - description : String? = nil, - date : Date? = nil, + xAxisLabel : String? = nil, + description : String? = nil, + date : Date? = nil, colour : ColourStyle = ColourStyle(colour: .red) ) { self.upperValue = upperValue diff --git a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift index 572b25e1..b0f798eb 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Protocols/BarChartProtocols.swift @@ -79,7 +79,9 @@ public protocol CTStandardBarChartDataSet: CTSingleDataSetProtocol { */ public protocol CTMultiBarChartDataSet: CTSingleDataSetProtocol {} - +/** + A protocol to extend functionality of `CTSingleDataSetProtocol` specifically for Ranged Bar Charts. + */ public protocol CTRangedBarChartDataSet: CTStandardBarChartDataSet {} @@ -122,7 +124,6 @@ public protocol CTMultiBarDataPoint: CTBarDataPointBaseProtocol, CTStandardDataP 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/Style/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift index 4bdaeb34..74069b56 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift @@ -13,17 +13,6 @@ import SwiftUI Controls the look of the chart as a whole, not including any styling specific to the data set(s), - - # Example -``` - BarChartStyle(infoBoxPlacement : .floating, - markerType : .full, - xAxisLabelPosition : .bottom, - xAxisLabelsFrom : .dataPoint, - yAxisLabelPosition : .leading, - yAxisNumberOfLabels: 5, - globalAnimation : .linear(duration: 1)) - ``` */ public struct BarChartStyle: CTBarChartStyle { @@ -65,11 +54,13 @@ public struct BarChartStyle: CTBarChartStyle { /// - 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. diff --git a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift index 3ee6ae54..98a47aa5 100644 --- a/Sources/SwiftUICharts/BarChart/Views/BarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/BarChart.swift @@ -22,16 +22,22 @@ import SwiftUI as the modifiers are various types for stacks that wrap around the previous views. ``` -.touchOverlay(chartData: data) -.averageLine(chartData: data) -.yAxisPOI(chartData: data) -.xAxisGrid(chartData: data) -.yAxisGrid(chartData: data) -.xAxisLabels(chartData: data) -.yAxisLabels(chartData: data) -.infoBox(chartData: data) -.headerBox(chartData: data) -.legends(chartData: data) + .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 { diff --git a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift index 1700cd83..79ef419d 100644 --- a/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/GroupedBarChart.swift @@ -22,16 +22,22 @@ import SwiftUI as the modifiers are various types for stacks that wrap around the previous views. ``` - .touchOverlay(chartData: data) - .averageLine(chartData: data) - .yAxisPOI(chartData: data) - .xAxisGrid(chartData: data) - .yAxisGrid(chartData: data) - .xAxisLabels(chartData: data) - .yAxisLabels(chartData: data) - .infoBox(chartData: data) - .headerBox(chartData: data) - .legends(chartData: data) + .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 { diff --git a/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift index 219af14e..7ebd4a09 100644 --- a/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/RangedBarChart.swift @@ -10,11 +10,11 @@ import SwiftUI /** View for creating a grouped bar chart. - Uses `GroupedBarChartData` data model. + Uses `RangedBarChartData` data model. # Declaration ``` - GroupedBarChart(chartData: data, groupSpacing: 25) + RangedBarChart(chartData: data) ``` # View Modifiers @@ -23,8 +23,13 @@ import SwiftUI around the previous views. ``` .touchOverlay(chartData: data) - .averageLine(chartData: data) - .yAxisPOI(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) diff --git a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift index 023fdace..b192948b 100644 --- a/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift +++ b/Sources/SwiftUICharts/BarChart/Views/StackedBarChart.swift @@ -22,16 +22,22 @@ import SwiftUI as the modifiers are various types for stacks that wrap around the previous views. ``` - .touchOverlay(chartData: data) - .averageLine(chartData: data) - .yAxisPOI(chartData: data) - .xAxisGrid(chartData: data) - .yAxisGrid(chartData: data) - .xAxisLabels(chartData: data) - .yAxisLabels(chartData: data) - .infoBox(chartData: data) - .headerBox(chartData: data) - .legends(chartData: data) + .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 { diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index 01b469c2..fbcb41f4 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -11,38 +11,6 @@ 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. - - # Example - ``` - static func weekOfData() -> LineChartData { - - let data = LineDataSet(dataPoints: [ - LineChartDataPoint(value: 120, xAxisLabel: "M", pointLabel: "Monday"), - LineChartDataPoint(value: 190, xAxisLabel: "T", pointLabel: "Tuesday"), - LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), - LineChartDataPoint(value: 175, xAxisLabel: "T", pointLabel: "Thursday"), - LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), - LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), - LineChartDataPoint(value: 190, xAxisLabel: "S", pointLabel: "Sunday") - ], - legendTitle: "Test One", - pointStyle: PointStyle(), - style: LineStyle(colour: Color.red, lineType: .curvedLine)) - - return LineChartData(dataSets : data, - metadata : ChartMetadata(title: "Some Data", subtitle: "A Week"), - xAxisLabels : ["Monday", "Thursday", "Sunday"], - chartStyle : LineChartStyle(infoBoxPlacement : .floating, - markerType : .indicator(style: DotStyle()), - xAxisLabelPosition : .bottom, - xAxisLabelsFrom : .chartData, - yAxisLabelPosition : .leading, - yAxisNumberOfLabels : 7, - baseline : .minimumWithMaximum(of: 80), - globalAnimation : .easeOut(duration: 1))) - } - - ``` */ public final class LineChartData: CTLineChartDataProtocol { diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift index 5a50d27a..9b3e45ba 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -11,45 +11,6 @@ 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. - - # Example - ``` - static func weekOfData() -> MultiLineChartData { - - let data = MultiLineDataSet(dataSets: [ - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 60, xAxisLabel: "M", pointLabel: "Monday"), - LineChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"), - LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), - LineChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"), - LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), - LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), - LineChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") - ], - legendTitle: "Test One", - pointStyle: PointStyle(), - style: LineStyle(colour: Color.red)), - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 90, xAxisLabel: "M", pointLabel: "Monday"), - LineChartDataPoint(value: 60, xAxisLabel: "T", pointLabel: "Tuesday"), - LineChartDataPoint(value: 120, xAxisLabel: "W", pointLabel: "Wednesday"), - LineChartDataPoint(value: 85, xAxisLabel: "T", pointLabel: "Thursday"), - LineChartDataPoint(value: 140, xAxisLabel: "F", pointLabel: "Friday"), - LineChartDataPoint(value: 80, xAxisLabel: "S", pointLabel: "Saturday"), - LineChartDataPoint(value: 50, xAxisLabel: "S", pointLabel: "Sunday") - ], - legendTitle: "Test Two", - pointStyle: PointStyle(), - style: LineStyle(colour: Color.blue))]) - - return MultiLineChartData(dataSets: data, - metadata: ChartMetadata(title: "Some Data", subtitle: "A Week"), - xAxisLabels: ["Monday", "Thursday", "Sunday"], - chartStyle: LineChartStyle(infoBoxPlacement: .fixed, - markerType: .full(attachment: .line(dot: .style(DotStyle()))), - baseline: .minimumWithMaximum(of: 40))) - } - ``` */ public final class MultiLineChartData: CTLineChartDataProtocol { diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift index 1af08529..7a020f0f 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift @@ -7,6 +7,11 @@ 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 @@ -24,7 +29,7 @@ public final class RangedLineChartData: CTLineChartDataProtocol { public var chartType : (chartType: ChartType, dataSetType: DataSetType) // MARK: Initializer - /// Initialises a Single Line Chart. + /// Initialises a ranged line chart. /// /// - Parameters: /// - dataSets: Data to draw and style a line. diff --git a/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift index 37f9b2fd..d9049dd5 100644 --- a/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataSet/LineDataSet.swift @@ -11,22 +11,6 @@ import SwiftUI Data set for a single line Contains information specific to each line within the chart . - - # Example - ``` - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 120, xAxisLabel: "M", pointLabel: "Monday"), - LineChartDataPoint(value: 190, xAxisLabel: "T", pointLabel: "Tuesday"), - LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), - LineChartDataPoint(value: 175, xAxisLabel: "T", pointLabel: "Thursday"), - LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), - LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), - LineChartDataPoint(value: 190, xAxisLabel: "S", pointLabel: "Sunday") - ], - legendTitle: "Test One", - pointStyle: PointStyle(), - style: LineStyle(colour: Color.red, lineType: .curvedLine)) - ``` */ public struct LineDataSet: CTLineChartDataSet { diff --git a/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift index 9ad14b60..0a5c67e8 100644 --- a/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataSet/MultiLineDataSet.swift @@ -11,36 +11,6 @@ import SwiftUI Data set containing multiple data sets for multiple lines Contains information about each of lines within the chart. - - # Example - ``` -MultiLineDataSet(dataSets: [ - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 60, xAxisLabel: "M", pointLabel: "Monday"), - LineChartDataPoint(value: 90, xAxisLabel: "T", pointLabel: "Tuesday"), - LineChartDataPoint(value: 100, xAxisLabel: "W", pointLabel: "Wednesday"), - LineChartDataPoint(value: 75, xAxisLabel: "T", pointLabel: "Thursday"), - LineChartDataPoint(value: 160, xAxisLabel: "F", pointLabel: "Friday"), - LineChartDataPoint(value: 110, xAxisLabel: "S", pointLabel: "Saturday"), - LineChartDataPoint(value: 90, xAxisLabel: "S", pointLabel: "Sunday") - ], - legendTitle: "Test One", - pointStyle: PointStyle(), - style: LineStyle(colour: Color.red)), - LineDataSet(dataPoints: [ - LineChartDataPoint(value: 90, xAxisLabel: "M", pointLabel: "Monday"), - LineChartDataPoint(value: 60, xAxisLabel: "T", pointLabel: "Tuesday"), - LineChartDataPoint(value: 120, xAxisLabel: "W", pointLabel: "Wednesday"), - LineChartDataPoint(value: 85, xAxisLabel: "T", pointLabel: "Thursday"), - LineChartDataPoint(value: 140, xAxisLabel: "F", pointLabel: "Friday"), - LineChartDataPoint(value: 80, xAxisLabel: "S", pointLabel: "Saturday"), - LineChartDataPoint(value: 50, xAxisLabel: "S", pointLabel: "Sunday") - ], - legendTitle: "Test Two", - pointStyle: PointStyle(), - style: LineStyle(colour: Color.blue)) - ]) - ``` */ public struct MultiLineDataSet: CTMultiLineChartDataSet { diff --git a/Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift b/Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift index da943e32..5fe00e8d 100644 --- a/Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift +++ b/Sources/SwiftUICharts/LineChart/Models/DataSet/RangedLineDataSet.swift @@ -11,24 +11,6 @@ import SwiftUI Data set for a ranged line. Contains information specific to the line and range fill. - - # Example - ``` - RangedLineDataSet(dataPoints: [ - RangedLineChartDataPoint(value: 10, upperValue: 20, lowerValue: 0 , xAxisLabel: "M", pointLabel: "Monday"), - RangedLineChartDataPoint(value: 25, upperValue: 35, lowerValue: 15, xAxisLabel: "T", pointLabel: "Tuesday"), - RangedLineChartDataPoint(value: 13, upperValue: 23, lowerValue: 3 , xAxisLabel: "W", pointLabel: "Wednesday"), - RangedLineChartDataPoint(value: 24, upperValue: 34, lowerValue: 14, xAxisLabel: "T", pointLabel: "Thursday"), - RangedLineChartDataPoint(value: 36, upperValue: 46, lowerValue: 26, xAxisLabel: "F", pointLabel: "Friday"), - RangedLineChartDataPoint(value: 14, upperValue: 24, lowerValue: 4 , xAxisLabel: "S", pointLabel: "Saturday"), - RangedLineChartDataPoint(value: 20, upperValue: 30, lowerValue: 10, xAxisLabel: "S", pointLabel: "Sunday") - ], - legendTitle: "Steps", - pointStyle: PointStyle(), - style: RangedLineStyle(lineColour: ColourStyle(colour: .red), - fillColour: ColourStyle(colour: Color(.blue).opacity(0.25)), - lineType: .curvedLine)) - ``` */ public struct RangedLineDataSet: CTRangedLineChartDataSet { @@ -39,8 +21,7 @@ public struct RangedLineDataSet: CTRangedLineChartDataSet { public var pointStyle : PointStyle public var style : RangedLineStyle - - /// Initialises a data set for a line in a Line Chart. + /// Initialises a data set for a line in a ranged line chart. /// - Parameters: /// - dataPoints: Array of elements. /// - legendTitle: Label for the data in legend. diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index 9961b10f..5210f665 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -39,6 +39,9 @@ public protocol CTLineChartDataProtocol: CTLineBarChartDataProtocol { */ 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 } @@ -91,10 +94,17 @@ public protocol CTLineChartDataSet: CTSingleDataSetProtocol { */ 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 {} diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index 490942b0..37cc8411 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -9,7 +9,9 @@ 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, diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift index 5c7b0f7d..a0bb144c 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift @@ -12,23 +12,6 @@ import SwiftUI Controls the look of the chart as a whole, not including any styling specific to the data set(s), - - # Example - ``` - LineChartStyle(infoBoxPlacement : .floating, - markerType : .indicator(style: DotStyle()), - xAxisGridStyle : GridStyle(), - xAxisLabelPosition : .bottom, - xAxisLabelColour : Color.primary, - xAxisLabelsFrom : .chartData, - yAxisGridStyle : GridStyle(), - yAxisLabelPosition : .leading, - yAxisLabelColour : Color.primary, - yAxisNumberOfLabels : 7, - baseline : .minimumWithMaximum(of: 80), - globalAnimation : .easeOut(duration: 1)) - ``` - */ public struct LineChartStyle: CTLineChartStyle { @@ -69,11 +52,13 @@ public struct LineChartStyle: CTLineChartStyle { /// - 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. diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift index d7f1f7c9..de9096c4 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Style/LineStyle.swift @@ -8,14 +8,7 @@ import SwiftUI /** - Model for controlling the aesthetic of the line chart. - - # Example - ``` - LineStyle(colour : .red, - lineType : .curvedLine, - strokeStyle: Stroke(lineWidth: 2)) - ``` + Model for controlling the styling for individual lines. */ public struct LineStyle: CTLineStyle, Hashable { @@ -32,8 +25,7 @@ public struct LineStyle: CTLineStyle, Hashable { */ public var ignoreZero : Bool - // MARK: - Single colour - /// Single Colour + /// Style of the line. /// - Parameters: /// - lineColour: Colour styling of the line. /// - lineType: Drawing style of the line diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/RangedLineStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/RangedLineStyle.swift index daa0d7c9..8b24a97d 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Style/RangedLineStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Style/RangedLineStyle.swift @@ -8,13 +8,6 @@ import SwiftUI /** Model for controlling the aesthetic of the ranged line chart. - - # Example - ``` - RangedLineStyle(lineColour: ColourStyle(colour: .red), - fillColour: ColourStyle(colour: Color(.blue).opacity(0.25)), - lineType : .curvedLine)) - ``` */ public struct RangedLineStyle: CTRangedLineStyle, Hashable { diff --git a/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift index aac9b3fc..82d719f7 100644 --- a/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift +++ b/Sources/SwiftUICharts/LineChart/ViewModifiers/PointMarkers.swift @@ -11,12 +11,12 @@ 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 @@ -25,7 +25,7 @@ internal struct PointMarkers: ViewModifier where T: CTLineChartDataProtocol { internal func body(content: Content) -> some View { ZStack { if chartData.isGreaterThanTwo() { - content + content chartData.getPointMarker() } else { content } } @@ -36,7 +36,7 @@ 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 CTChartData + 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. @@ -46,18 +46,20 @@ extension View { # 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 - - Multi Layer 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 index dcd746f1..65f8f099 100644 --- a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift @@ -36,6 +36,7 @@ import SwiftUI .xAxisLabels(chartData: data) .yAxisLabels(chartData: data) .infoBox(chartData: data) + .floatingInfoBox(chartData: data) .headerBox(chartData: data) .legends(chartData: data) ``` diff --git a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift index 2f04e4cd..9c519a72 100644 --- a/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift +++ b/Sources/SwiftUICharts/LineChart/Views/LineChartView.swift @@ -39,6 +39,7 @@ import SwiftUI .xAxisLabels(chartData: data) .yAxisLabels(chartData: data) .infoBox(chartData: data) + .floatingInfoBox(chartData: data) .headerBox(chartData: data) .legends(chartData: data, columns: [GridItem(.flexible()), GridItem(.flexible())]) ``` diff --git a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift index 4679d62c..a75bcfaf 100644 --- a/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/MultiLineChart.swift @@ -36,6 +36,7 @@ import SwiftUI .xAxisLabels(chartData: data) .yAxisLabels(chartData: data) .infoBox(chartData: data) + .floatingInfoBox(chartData: data) .headerBox(chartData: data) .legends(chartData: data) ``` diff --git a/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift index 2881c01f..f59cfbb9 100644 --- a/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift @@ -7,6 +7,43 @@ 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 diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift index 2b779a6f..4124cec2 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/DoughnutChartData.swift @@ -11,23 +11,6 @@ import SwiftUI Data for drawing and styling a doughnut chart. This model contains the data and styling information for a doughnut chart. - - # Example - ``` - static func makeData() -> DoughnutChartData { - let data = PieDataSet(dataPoints: [PieChartDataPoint(value: 7, description: "One", colour: .blue), - PieChartDataPoint(value: 2, description: "Two", colour: .red), - PieChartDataPoint(value: 9, description: "Three", colour: .purple), - PieChartDataPoint(value: 6, description: "Four", colour: .green), - PieChartDataPoint(value: 4, description: "Five", colour: .orange)], - legendTitle: "Data") - - return DoughnutChartData(dataSets: data, - metadata: ChartMetadata(title: "Pie", subtitle: "mmm doughnuts"), - chartStyle: DoughnutChartStyle(infoBoxPlacement: .header), - noDataText: Text("No Data")) - } - ``` */ public final class DoughnutChartData: CTDoughnutChartDataProtocol { @@ -43,7 +26,7 @@ public final class DoughnutChartData: CTDoughnutChartDataProtocol { public final var chartType : (chartType: ChartType, dataSetType: DataSetType) // MARK: Initializer - /// Initialises a Doughnut Chart. + /// Initialises Doughnut Chart data. /// /// - Parameters: /// - dataSets: Data to draw and style the chart. diff --git a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift index a641bddf..c6546364 100644 --- a/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift +++ b/Sources/SwiftUICharts/PieChart/Models/ChartData/PieChartData.swift @@ -11,23 +11,6 @@ import SwiftUI Data for drawing and styling a pie chart. This model contains the data and styling information for a pie chart. - - # Example - ``` - static func makeData() -> PieChartData { - let data = PieDataSet(dataPoints: [ - PieChartDataPoint(value: 7, description: "One", colour: .blue), - PieChartDataPoint(value: 2, description: "Two", colour: .red), - PieChartDataPoint(value: 9, description: "Three", colour: .purple), - PieChartDataPoint(value: 6, description: "Four", colour: .green), - PieChartDataPoint(value: 4, description: "Five", colour: .orange)], - legendTitle: "Data") - - return PieChartData(dataSets: data, - metadata: ChartMetadata(title: "Pie", subtitle: "mmm pie"), - chartStyle: PieChartStyle(infoBoxPlacement: .header)) - } - ``` */ public final class PieChartData: CTPieChartDataProtocol { @@ -43,7 +26,7 @@ public final class PieChartData: CTPieChartDataProtocol { public final var chartType: (chartType: ChartType, dataSetType: DataSetType) // MARK: Initializer - /// Initialises a Pie Chart. + /// Initialises Pie Chart data. /// /// - Parameters: /// - dataSets: Data to draw and style the chart. diff --git a/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift b/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift index 5634e4f9..11d867e2 100644 --- a/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift +++ b/Sources/SwiftUICharts/PieChart/Models/DataPoints/PieChartDataPoint.swift @@ -9,13 +9,6 @@ import SwiftUI /** Data for a single segement of a pie chart. - - # Example - ``` - PieChartDataPoint(value: 7, - description: "One", - colour: .blue), - ``` */ public struct PieChartDataPoint: CTPieDataPoint { diff --git a/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift b/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift index ab5ff4d5..2f19f8c3 100644 --- a/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift +++ b/Sources/SwiftUICharts/PieChart/Models/DataSets/PieDataSet.swift @@ -9,17 +9,6 @@ import SwiftUI /** Data set for a pie chart. - - # Example - ``` - PieDataSet(dataPoints: [ - PieChartDataPoint(value: 7, description: "One", colour: .blue), - PieChartDataPoint(value: 2, description: "Two", colour: .red), - PieChartDataPoint(value: 9, description: "Three", colour: .purple), - PieChartDataPoint(value: 6, description: "Four", colour: .green), - PieChartDataPoint(value: 4, description: "Five", colour: .orange)], - legendTitle: "Data") - ``` */ public struct PieDataSet: CTSingleDataSetProtocol { diff --git a/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift index dde6e9da..68b93f0d 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift @@ -9,12 +9,6 @@ import SwiftUI /** Model for controlling the overall aesthetic of the chart. - - ``` - DoughnutChartStyle(infoBoxPlacement: .floating, - globalAnimation: .linear(duration: 1), - strokeWidth: 60) - ``` */ public struct DoughnutChartStyle: CTDoughnutChartStyle { diff --git a/Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift index c586bf27..8f5625e5 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift @@ -9,13 +9,6 @@ import SwiftUI /** Model for controlling the overall aesthetic of the chart. - - ``` - PieChartStyle(infoBoxPlacement: .fixed, - infoBoxValueColour: Color.primary, - infoBoxDescriptionColour: Color(.systemBackground), - globalAnimation: .linear(duration: 1)) - ``` */ public struct PieChartStyle: CTPieChartStyle { diff --git a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift index 4aef5b41..2658876c 100644 --- a/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/DoughnutChart.swift @@ -24,11 +24,11 @@ import SwiftUI ``` .touchOverlay(chartData: data) .infoBox(chartData: data) + .floatingInfoBox(chartData: data) .headerBox(chartData: data) .legends(chartData: data) ``` */ -// .stroke -- REMOVE FORCE UNWRAP public struct DoughnutChart: View where ChartData: DoughnutChartData { @ObservedObject var chartData: ChartData @@ -68,4 +68,5 @@ public struct DoughnutChart: View where ChartData: DoughnutChartData self.startAnimation = false } } + } diff --git a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift index 5f402eb0..90bd25b3 100644 --- a/Sources/SwiftUICharts/PieChart/Views/PieChart.swift +++ b/Sources/SwiftUICharts/PieChart/Views/PieChart.swift @@ -24,6 +24,7 @@ import SwiftUI ``` .touchOverlay(chartData: data) .infoBox(chartData: data) + .floatingInfoBox(chartData: data) .headerBox(chartData: data) .legends(chartData: data) ``` diff --git a/Sources/SwiftUICharts/Shared/API.swift b/Sources/SwiftUICharts/Shared/API.swift index c8fd88fd..d00fa25f 100644 --- a/Sources/SwiftUICharts/Shared/API.swift +++ b/Sources/SwiftUICharts/Shared/API.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + Displays the data points value with the unit. + */ public struct InfoValue : View where T: CTChartData { @ObservedObject var chartData: T @@ -22,6 +25,9 @@ public struct InfoValue : View where T: CTChartData { } } +/** + Displays the data points description. + */ public struct InfoDescription : View where T: CTChartData { @ObservedObject var chartData: T @@ -37,6 +43,9 @@ public struct InfoDescription : View where T: CTChartData { } } +/** + Option the as a String between the Value and the Description. + */ public struct InfoExtra: View where T: CTChartData { @ObservedObject var chartData: T @@ -58,6 +67,12 @@ public struct InfoExtra: View where T: CTChartData { } 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 { @@ -184,6 +199,12 @@ extension LegendData { } } } + /** + 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 { diff --git a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift index a671c129..0a1949cd 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift @@ -11,11 +11,6 @@ import SwiftUI Data model for the chart's metadata Contains the Title, Subtitle and colour information for them. - - # Example - ``` - ChartMetadata(title: "Some Data", subtitle: "A weeks worth") - ``` */ public struct ChartMetadata { /// The charts title diff --git a/Sources/SwiftUICharts/Shared/Models/ColourStyle.swift b/Sources/SwiftUICharts/Shared/Models/ColourStyle.swift index 91273bb3..97100366 100644 --- a/Sources/SwiftUICharts/Shared/Models/ColourStyle.swift +++ b/Sources/SwiftUICharts/Shared/Models/ColourStyle.swift @@ -7,14 +7,8 @@ import SwiftUI -//MARK: - Line /** - Model for controlling the colours of `Stroke`. - - # Example - ``` - ColourStyle(colour: .red) - ``` + Model for setting up colour styling. */ public struct ColourStyle: CTColourStyle, Hashable { diff --git a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift index aaa811ad..f9942375 100644 --- a/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift +++ b/Sources/SwiftUICharts/Shared/Models/InfoViewData.swift @@ -8,7 +8,7 @@ import SwiftUI /** - Data model to pass view information internally for the `InfoBox` and `HeaderBox`. + Data model to pass view information internally for the `InfoBox`, `FloatingInfoBox` and `HeaderBox`. */ public struct InfoViewData { @@ -17,7 +17,7 @@ public struct InfoViewData { Set from TouchOverlay via the relevant protocol. - Used by `HeaderBox` and `InfoBox`. + Used by `InfoBox`, `FloatingInfoBox` and `HeaderBox`. */ var isTouchCurrent: Bool = false @@ -26,7 +26,7 @@ public struct InfoViewData { Set from TouchOverlay via the relevant protocol. - Used by `HeaderBox` and `InfoBox`. + Used by `InfoBox`, `FloatingInfoBox` and `HeaderBox`. */ var touchOverlayInfo: [DP] = [] @@ -35,7 +35,7 @@ public struct InfoViewData { Set from TouchOverlay via the relevant protocol. - Used by `HeaderBox` and `InfoBox`. + Used by `InfoBox`, `FloatingInfoBox` and `HeaderBox`. */ var touchSpecifier: String = "%.0f" @@ -46,7 +46,7 @@ public struct InfoViewData { Set from TouchOverlay via the relevant protocol. - Used by `HeaderBox` and `InfoBox`. + Used by `InfoBox`, `FloatingInfoBox` and `HeaderBox`. */ var touchLocation: CGPoint = .zero @@ -57,7 +57,7 @@ public struct InfoViewData { Set from TouchOverlay via the relevant protocol. - Used by `HeaderBox` and `InfoBox`. + Used by `InfoBox`, `FloatingInfoBox` and `HeaderBox`. */ var chartSize: CGRect = .zero diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 43e3ca5c..6d596dd7 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -172,6 +172,7 @@ public protocol CTSingleDataSetProtocol: CTDataSetProtocol { 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 diff --git a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift index 4d9addd4..da097e80 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocolsExtensions.swift @@ -33,6 +33,13 @@ extension CTChartData { } 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: @@ -43,10 +50,24 @@ extension CTChartData { 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))") } - public func infoUnit(info: DataPoint) -> some View { + + /** + 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("") @@ -56,9 +77,23 @@ extension CTChartData { 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 && @@ -70,15 +105,16 @@ extension CTChartData { } } } + extension CTChartData { - /// Sets the point info box location while keeping it within the parent view. + + /// 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) { @@ -94,6 +130,7 @@ extension CTChartData { 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. */ @@ -103,6 +140,7 @@ extension CTSingleDataSetProtocol where Self.DataPoint: CTStandardDataPointProto /** Returns the lowest value in the data set. + - Parameter dataSet: Target data set. - Returns: Lowest value in data set. */ @@ -112,6 +150,7 @@ extension CTSingleDataSetProtocol where Self.DataPoint: CTStandardDataPointProto /** Returns the average value from the data set. + - Parameter dataSet: Target data set. - Returns: Average of values in data set. */ @@ -124,6 +163,7 @@ extension CTSingleDataSetProtocol where Self.DataPoint: CTStandardDataPointProto 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. */ @@ -133,6 +173,7 @@ extension CTSingleDataSetProtocol where Self.DataPoint: CTRangeDataPointProtocol /** Returns the lowest value in the data set. + - Parameter dataSet: Target data set. - Returns: Lowest value in data set. */ @@ -142,6 +183,7 @@ extension CTSingleDataSetProtocol where Self.DataPoint: CTRangeDataPointProtocol /** Returns the average value from the data set. + - Parameter dataSet: Target data set. - Returns: Average of values in data set. */ @@ -155,6 +197,7 @@ extension CTSingleDataSetProtocol where Self.DataPoint: CTRangeDataPointProtocol 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. */ @@ -168,6 +211,7 @@ extension CTMultiDataSetProtocol where Self.DataSet.DataPoint: CTStandardDataPoi /** Returns the lowest value in the data sets. + - Parameter dataSet: Target data sets. - Returns: Lowest value in data sets. */ @@ -181,6 +225,7 @@ extension CTMultiDataSetProtocol where Self.DataSet.DataPoint: CTStandardDataPoi /** Returns the average value from the data sets. + - Parameter dataSet: Target data sets. - Returns: Average of values in data sets. */ @@ -197,27 +242,33 @@ extension CTMultiDataSetProtocol where Self.DataSet.DataPoint: CTStandardDataPoi // 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/Types/GradientStop.swift b/Sources/SwiftUICharts/Shared/Types/GradientStop.swift index 1b8d962d..e265179a 100644 --- a/Sources/SwiftUICharts/Shared/Types/GradientStop.swift +++ b/Sources/SwiftUICharts/Shared/Types/GradientStop.swift @@ -8,7 +8,7 @@ import SwiftUI /** - A mediator for `Gradient.Stop`. to allow it to be stored in `LegendData`. + A mediator for `Gradient.Stop` to allow it to be stored in `LegendData`. Gradient.Stop doesn't conform to Hashable. */ diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/FloatingInfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/FloatingInfoBox.swift index 7ba0a7ea..750c8387 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/FloatingInfoBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/FloatingInfoBox.swift @@ -7,6 +7,9 @@ import SwiftUI +/** + A view that displays information from `TouchOverlay`. + */ internal struct FloatingInfoBox: ViewModifier where T: CTChartData { @ObservedObject var chartData: T @@ -50,6 +53,8 @@ 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. diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index b960b8d4..1b8fc1eb 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -105,14 +105,12 @@ extension View { Displays the metadata about the chart. Adds a view above the chart that displays the title and subtitle. - infoBoxPlacement is set to .header then the datapoint info will + 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. - - - Tag: HeaderBox */ public func headerBox(chartData: T) -> some View { self.modifier(HeaderBox(chartData: chartData)) diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 7869e869..f7328b59 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -61,12 +61,15 @@ extension View { Adds an overlay to detect touch and display the relivent information from the nearest data point. - Requires: - If LineChartStyle --> infoBoxPlacement is set to .header + If ChartStyle --> infoBoxPlacement is set to .header then `.headerBox` is required. - If LineChartStyle --> infoBoxPlacement is set to .fixed or . floating + 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 diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift index 54448e93..c35bf6f2 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/ChartViewData.swift @@ -1,6 +1,5 @@ // // ChartViewData.swift -// LineChart // // Created by Will Dale on 03/01/2021. // @@ -9,11 +8,22 @@ 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 diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/GridStyle.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/GridStyle.swift index 6e91d668..49cab48f 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/GridStyle.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/GridStyle.swift @@ -9,15 +9,6 @@ import SwiftUI /** Controlling for the look of the Grid - - # Example - ``` - GridStyle(numberOfLines: 7, - lineColour : .gray, - lineWidth : 1, - dash : [8], - dashPhase : 0) - ``` */ public struct GridStyle { diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index 3b6cb77f..200693ea 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -86,8 +86,6 @@ public protocol CTLineBarChartStyle: CTChartStyle { */ var markerType : Mark { get set } - - /** Style of the vertical lines breaking up the chart. */ @@ -108,9 +106,11 @@ public protocol CTLineBarChartStyle: CTChartStyle { */ 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. */ @@ -131,6 +131,9 @@ public protocol CTLineBarChartStyle: CTChartStyle { */ var yAxisNumberOfLabels: Int { get set } + /** + Label to display next to the chart giving info about the axis. + */ var yAxisTitle: String? { get set } /** @@ -158,6 +161,9 @@ public protocol CTLineBarDataPointProtocol: CTDataPointBaseProtocol { } extension CTLineBarDataPointProtocol { + /** + Unwarpped xAxisLabel + */ var wrappedXAxisLabel : String { self.xAxisLabel ?? "" } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift index d7cc5772..cf3157f6 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisGrid.swift @@ -38,7 +38,7 @@ extension View { /** Adds vertical lines along the X axis. - The style is set in ChartData --> LineChartStyle --> xAxisGridStyle + The style is set in ChartData --> ChartStyle --> xAxisGridStyle - Requires: Chart Data to conform to CTLineBarChartDataProtocol. @@ -46,14 +46,16 @@ extension View { # 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 - - Multi Layer Pie Chart - Parameter chartData: Chart data model. - Returns: A new view containing the chart with vertical lines under it. diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift index 9a9892d8..cbf55906 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/XAxisLabels.swift @@ -64,17 +64,22 @@ extension View { - 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 - - Multi Layer Pie Chart - Parameter chartData: Chart data model. - Returns: A new view containing the chart with labels marking the x axis. diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift index 5d8d8202..bb28dfa6 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisGrid.swift @@ -41,18 +41,20 @@ extension View { - 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 - - Multi Layer Pie Chart - Parameter chartData: Chart data model. - Returns: A new view containing the chart with horizontal lines under it. diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift index c1e8461b..7d6149b8 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisLabels.swift @@ -120,14 +120,16 @@ extension View { # 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 - - Multi Layer Pie Chart - Parameters: - specifier: Decimal precision specifier diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index 8a20f67b..b1b8d003 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -177,14 +177,16 @@ extension View { # 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 - - Multi Layer Pie Chart - Parameters: - chartData: Chart data model. @@ -196,15 +198,16 @@ extension View { - 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) + */ + 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, @@ -246,14 +249,16 @@ extension View { # 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 - - Multi Layer Pie Chart - Parameters: - chartData: Chart data model. @@ -267,13 +272,14 @@ extension View { - 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) + 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, diff --git a/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift index c58ec3d1..3365fc00 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/GroupedBarChartTests.swift @@ -28,31 +28,31 @@ final class GroupedBarChartTests: XCTestCase { let data = MultiBarDataSets(dataSets: [ MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", description: "One One" , group: Group.one.data), - MultiBarChartDataPoint(value: 50, xAxisLabel: "1.2", description: "One Two" , group: Group.two.data), - MultiBarChartDataPoint(value: 30, xAxisLabel: "1.3", description: "One Three" , group: Group.three.data), - MultiBarChartDataPoint(value: 40, xAxisLabel: "1.4", description: "One Four" , group: Group.four.data) + 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, xAxisLabel: "2.1", description: "Two One" , group: Group.one.data), - MultiBarChartDataPoint(value: 60, xAxisLabel: "2.2", description: "Two Two" , group: Group.two.data), - MultiBarChartDataPoint(value: 40, xAxisLabel: "2.3", description: "Two Three" , group: Group.three.data), - MultiBarChartDataPoint(value: 60, xAxisLabel: "2.3", description: "Two Four" , group: Group.four.data) + 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, xAxisLabel: "3.1", description: "Three One" , group: Group.one.data), - MultiBarChartDataPoint(value: 70, xAxisLabel: "3.2", description: "Three Two" , group: Group.two.data), - MultiBarChartDataPoint(value: 30, xAxisLabel: "3.3", description: "Three Three", group: Group.three.data), - MultiBarChartDataPoint(value: 90, xAxisLabel: "3.4", description: "Three Four" , group: Group.four.data) + 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, xAxisLabel: "4.1", description: "Four One" , group: Group.one.data), - MultiBarChartDataPoint(value: 80, xAxisLabel: "4.2", description: "Four Two" , group: Group.two.data), - MultiBarChartDataPoint(value: 20, xAxisLabel: "4.3", description: "Four Three" , group: Group.three.data), - MultiBarChartDataPoint(value: 50, xAxisLabel: "4.3", description: "Four Four" , group: Group.four.data) + 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) ]) ]) @@ -84,20 +84,20 @@ final class GroupedBarChartTests: XCTestCase { func testGroupedBarIsGreaterThanTwoFalse() { let data = MultiBarDataSets(dataSets: [ MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", description: "One One" , group: Group.one.data) + MultiBarChartDataPoint(value: 10, description: "One One" , group: Group.one.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 20, xAxisLabel: "2.1", description: "Two One" , group: Group.one.data) + MultiBarChartDataPoint(value: 20, description: "Two One" , group: Group.one.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 30, xAxisLabel: "3.1", description: "Three One", group: Group.one.data) + MultiBarChartDataPoint(value: 30, description: "Three One", group: Group.one.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", description: "Four One" , group: Group.one.data) + MultiBarChartDataPoint(value: 40, description: "Four One" , group: Group.one.data) ]) ]) let chartData = GroupedBarChartData(dataSets: data, groups: groups) diff --git a/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift index 48f3b6d3..4f9ac532 100644 --- a/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift +++ b/Tests/SwiftUIChartsTests/BarCharts/StackedBarChartTests.swift @@ -28,31 +28,31 @@ final class StackedBarChartTests: XCTestCase { let data = MultiBarDataSets(dataSets: [ MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", description: "One One" , group: Group.one.data), - MultiBarChartDataPoint(value: 50, xAxisLabel: "1.2", description: "One Two" , group: Group.two.data), - MultiBarChartDataPoint(value: 30, xAxisLabel: "1.3", description: "One Three" , group: Group.three.data), - MultiBarChartDataPoint(value: 40, xAxisLabel: "1.4", description: "One Four" , group: Group.four.data) + 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, xAxisLabel: "2.1", description: "Two One" , group: Group.one.data), - MultiBarChartDataPoint(value: 60, xAxisLabel: "2.2", description: "Two Two" , group: Group.two.data), - MultiBarChartDataPoint(value: 40, xAxisLabel: "2.3", description: "Two Three" , group: Group.three.data), - MultiBarChartDataPoint(value: 60, xAxisLabel: "2.3", description: "Two Four" , group: Group.four.data) + 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, xAxisLabel: "3.1", description: "Three One" , group: Group.one.data), - MultiBarChartDataPoint(value: 70, xAxisLabel: "3.2", description: "Three Two" , group: Group.two.data), - MultiBarChartDataPoint(value: 30, xAxisLabel: "3.3", description: "Three Three", group: Group.three.data), - MultiBarChartDataPoint(value: 90, xAxisLabel: "3.4", description: "Three Four" , group: Group.four.data) + 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, xAxisLabel: "4.1", description: "Four One" , group: Group.one.data), - MultiBarChartDataPoint(value: 80, xAxisLabel: "4.2", description: "Four Two" , group: Group.two.data), - MultiBarChartDataPoint(value: 20, xAxisLabel: "4.3", description: "Four Three" , group: Group.three.data), - MultiBarChartDataPoint(value: 50, xAxisLabel: "4.4", description: "Four Four" , group: Group.four.data) + 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) ]) ]) @@ -84,19 +84,19 @@ final class StackedBarChartTests: XCTestCase { func testStackedBarIsGreaterThanTwoFalse() { let data = MultiBarDataSets(dataSets: [ MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 10, xAxisLabel: "1.1", description: "One One" , group: Group.one.data) + MultiBarChartDataPoint(value: 10, description: "One One" , group: Group.one.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 20, xAxisLabel: "2.1", description: "Two One" , group: Group.one.data) + MultiBarChartDataPoint(value: 20, description: "Two One" , group: Group.one.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 30, xAxisLabel: "3.1", description: "Three One", group: Group.one.data) + MultiBarChartDataPoint(value: 30, description: "Three One", group: Group.one.data) ]), MultiBarDataSet(dataPoints: [ - MultiBarChartDataPoint(value: 40, xAxisLabel: "4.1", description: "Four One" , group: Group.one.data) + MultiBarChartDataPoint(value: 40, description: "Four One" , group: Group.one.data) ]) ]) let chartData = StackedBarChartData(dataSets: data, groups: groups) From 91c3287c90159c4a1d0464aedaf74738e5775107 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 13:37:12 +0000 Subject: [PATCH 142/152] Document and tidy. --- Resources/BarOne.png | Bin 39367 -> 0 bytes Resources/BarTwo.png | Bin 28567 -> 0 bytes Resources/LineOne.png | Bin 39026 -> 0 bytes Resources/LineTwo.png | Bin 44776 -> 0 bytes .../ChartData/StackedBarChartData.swift | 15 +++++++++++++++ .../BarChart/Models/Style/BarChartStyle.swift | 8 ++++++++ .../Models/Style/LineChartStyle.swift | 18 +++++++++++++----- .../Models/Style/DoughnutChartStyle.swift | 9 +++++++++ .../PieChart/Models/Style/PieChartStyle.swift | 10 +++++++++- .../Models/Protocols/SharedProtocols.swift | 14 +++++++++++++- .../ViewModifiers/FloatingInfoBox.swift | 3 +-- .../Shared/Views/TouchOverlayBox.swift | 4 ++++ 12 files changed, 72 insertions(+), 9 deletions(-) delete mode 100644 Resources/BarOne.png delete mode 100644 Resources/BarTwo.png delete mode 100644 Resources/LineOne.png delete mode 100644 Resources/LineTwo.png diff --git a/Resources/BarOne.png b/Resources/BarOne.png deleted file mode 100644 index 9f5aa69a8f5df7ab8265205d5b0ebe431f0b7e2d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39367 zcmZ^~1yo!?kTyCnzzo45xQ8IY-5D%E2%3=K?gV#t2rdZ(*AOIlfZ#IdV1WSw1b27W zfj8{#zq{xC@7+1~+@9{L?!JAytE#KMj#5`uz>2|AqGSQ5H3GwC6N2cQm!&^s;w);sS_!i6WEs7H%eVUiNkluA*KN zFaCo<6q$a?=6XT*A0%$J5-+q>)ahg$T`cGXIk`ExUqGJG(b0*!m|Kdze=YxC?8tu- zFRb0%oJ6^}JUu-*J^45tU97lxL_|cmxOusFc{z|29IoCDZYEwF4z3LUS;_yY=e32a znTw5+n~kFb-BZ0LrjG7z5-(mnHT2)tfA-VD%jSPta&Y~x*FwG^*HZ}>4<|R*{}-E^ zjphFj+f&JZvi-+<{bzIHPnC(P+jv>n>AbeFw{UPpb`8QS#K$ZCAMN~KMgP;&|HZ2P zzgW5Xh5t9}|0?=_v&uT!Il5>#nV4BXc>as!e`WnI>ZhKIs=3%$AP4nn=$^>`ue|@F z7w38!*Z(!n|M?OBBNzD)cfXrT8Pt;L5CfwlQApoGv0p@^5x)z`Lwh$K zcJ=7Lx3HA$Be6Blz49m9Q`H}`0a8VPe*V5_giBD%&)wVh+0XZw;FzZJNMKerf{IU z3^U9^(w(zAAfd>sSgpBb3S9mk${$Ycn2KS2Di$X{ES$7xZGhMm1Z9N1B|mc7>7s^x zduZl%w#8Dur_p@%hN>Q`-~jd~KuAWzPqm@LRSZ`d6;uOt_NNjm-ShLv9yRMRDXzjf ze2vAd$mjS1 z@xq~A3##ca!nb^t`PgCsZ+fP0T=mma<>@$E2(}Tz95DBXcwku) zP{*s03T8nXxDZ)Bz9HHi8FNG` zyUw^;ckZ3&(Rynv)@hoc*Jag!C#~k?YcM+&q1Kf0NF}=Zt2FSI@LgPj5>=gxe$j!8 zbJS7Z8Z={B>J>Lwj1%kSDwrX0-!l#3kS}M{=SYW56O?%;>ISx&zF`{31_xVwhE|^r z$P}pxi#RN}Paj}0-S1U(1cWOEP(&QB_QsUS#gKdyT5!|oU|6p?aCC763az-EP=hd> z0m0nZyH49%dle0azsKY$sqA>T)mPp+dRml04{=Z# zI{RO}Br6>=e;hRJt2;5qj&}CMW6}GfVCpv^B+5ENE>zz8TxTjWUU69LWN2wkGZjQ1 zhUrM&dvq48ylL6Yuq{>MZ2HlBIcrOKgVo--AvWLiA4^TgiB3Pw$vY?zv1f$%^xfOO zVZmeak7K}DTAkw1T1f&69={6dG-9c80z8*{R%IhOZrT{%Okryyzj8Ye@{E2RhRWLV z!@hZ^Xc9+gULA4h*%3qjc~_>p&c?3$rC`Y36tW!*XDEW4DuMXc^>E3v{VZGdPR5#k zUi;biFW>M^*4bzi62_@NRLv;sDTKF~2x74hxiBp2_G=sKs9Q{TMeFo-MHen1yOe27 zHE!Y&Fv#2D1PlJ14YYW)WCsM^%`J5;d@8GuAq$56>q6hR42;M6v8aZWCh7K%6h?%n zT{#>zOU^o8^s&}t`m1B*4-Z!zTBQ0;(mi~7+(}Pz(VptKNR;Tt=;1UKUgc4Q*|J~rX)R7D{@41z;u=c=S6^CZfc{;!Am$+jv3B|^q-cl@ADG!F z19$E0*N)=Sc<4^PbStdHP3l4kBSGEm!s05gY&mScrrS7K`}I2Qm*wWW#ymSIUC&PpV82dzEWu z8RL`Hbdzh2n?KqSW0mj{&gSj!{cE3fh=%tD_l3ECQW>L%1l?{)K9cJ(Lvqu?HAOa= zE(~1SnBJjALS7DVabjbce+_2FlEB7>>OH@b`ynloz=)SE zg3r}N*oF!h{kmpM8WtQ6yr4S1Zd{FKGcCUk{YazXUA`h@-m%8iX|RUM#@831Poosr z!ky)1i4EoJf5}}_CJPHzTn8kHXC8yO+G#eOH=IA)I#I1q9i0mtRK5%TMsC_cuGtLc zNE?mvP(7vyRN<>;>sW)nbcH50SM6bsS5AHA`HdVT%Gp@f@aHDBTGwqD%(N%ePa2Lp z4M?MBtkYdeX;t*~gjz`P@O743mDwB1((n|6V+q&48x=93ixm#;W%xcKT>MS5-4-@Z zF1pm&Ce96wy&S8ktX^=_o9OV!kJ7%{GX}TjE>Qaf=s%ymin5M}izna0!S5iYKGaWT%CUU#N?4uM!+T1Lm-RaJ%%p(-U zpp`!K)9!mEuC_WJHfop=ZDyh}OeTsAKb|2$jV$%X^PfpX^clDN^8Gww^+Qd9t+J`r z&$2*Lp){bwlHStol=CQFzIo#Yha-8{5W=DMk6_G0j{@T@aC$`e-sZX?@U9`xBqN@7 zy_xpc%Xya}4zsR$^)S@{jIs3OIsvBYVE~yW3nD%J((K5J%9wm$U(T#;8J#Oqqr+N! zKa~E*zy8OW(Ev?jW7_1oktJ7`#f zoc@}**-;A=4g2Q&Q_RYgoi1Hqf#4M-N8-$LD#~Nxjni|XRX>HzX zXc@Rq{-u35j$l#BO)iGq&5X*=*3UW2Rdwufy3&2QJWcKJ+vUr>QNCvUk<#s&?Zv!x zFh6tDuy(4i`=M_7;qHjmUxsMuZ&J?rk4WG9HCjo=iomx-n}5BxvW477M7GZ7QXbwK z-Ck4$N?eo~Ay{t-&4NzJ849Ml5#i!jElY?Z52QZ(neDmg{p@zmmfdE6++Qo{G?{6P zEFxB+kI`)SMuEeT(MjXj2X?2ZC}`%H(p!>E4VJ z38K3tKhH)b1LX@C@Np&YkMX~*PpDi4+{PH)ul___&Q;Mqbm9fzQ^$(`#k&RToGOOr z-c~){RzWyJ-cXDuTMhLqu68hUQWM!oek1!&I3R7W zee`7{^!6(7@#?36$4ucngVFkh^Hou#@Fs54JOXC+J4>3rMYz7Csdl+y3Y7+D#q~q`)*z+ zZYj-i5U6MTY`Y*Ko^d4ta-P3?KA~2nE#rs$NEPwItC8B#Z{J{KA6lJ?Km}2uQvm-x z8#P~DJ87@c=nYB4pwns;v;5Kv_WTcH9oR4`SLMP1o)AJizS}Bij3#f9w3E}uF3Fv0 zyT7X1Dtu=(ffl7ZxaKGr92{q*NTGg1j4DH+a?yU(R^>C|nnTn}_W|E-{FwE!=-<%S z%MXa9M}*P8<>n**Bg&A$JFUvhomw>%YZ9DR--SIxlZec=Xml)+4;gZ@PVX*-kb_oj z=GM@%z;QXYJ?!W^H^(Ztzf-Q}J*W|)X_fxsU$y3a^;N$i<6QxmwgUhABDlKka+ZzB zQZ=8l?i>8EYj}~>1o-(Ta>9j^Ns(#cTQGZx=I0T+t!&}okI$I-rVa2ny92QO$(@wztw#ES#fs| z4L>F;E%>sAu~PA-sf>5@Nk=P&8h(17&&?8OkTGO=^LeUE>$xRlb*zRP`#SgWCO3bO z=xX0evc)kF%cnAFXPuW_AKo9d;JGBRJz?^0>vt-?(L-7=4xC5DatztmhVgct^|jcv z3ErGDkHdABAmrk=zar*5Mv9-M1i=mLlwhtIL-gyR`ibkW%96bpRC9PmJLmO{wjYdf z%xWmT$QGl2=oRFz!3p5z=NCmjyBfI^FkE=)BGTl7&6KoVEsDjnR#ye#udtv7HlRs? zP%MIFvaH5wktV?;eFW8eiJJknO335QD9q)n*|RI;WH6f<_&Vlmv@Y}q2D$uo@+zy1 zH|0B(YhDttFG1?6cFLKqfe-X1_ildca%qJg&-+bbRS1DRWic(;w(NcxDppFP&cA#6PP3oj(IklXAnHj4?6jq!y9uqR*~GZDf)w z@MZ{V!Iy=oq(y8iIQKpKGmMHpdt0i#!{;tug)~LYk`N!Od_Fd2{g(0l&VE^p9{^Qk z&VFvy%M#kWo#atyMjGsil*F|OR`>mX5#=AL@*aQTS(tPOl*I49raVhfv(|bLmZe># z`x(#w!79hwlAX=yvL?0nZqvW&n+3rAv{73}xAsdePO{LjytDrdkBciQh|9;>@?W4NU6=>fIO{IAYp`9?~%gY3#(oId7O zuu(@vjtn#1J>HK#hDpgD|Dv+s3L8-pS!?&C=4x)7SqqwreSoRpKxXp{CUQhe)j1OT zUI+U9>!u<3HtyZFE$Juymmo|yD|U1AHEbL{^;X^+h{YhdGm{{vJ+APR2yf*c-Y{sD z>|%)(ag>&|lja`u1l7Us zYkg@rseQ5<`5N2nj`JKZapNLKqzYcV^i#kmUFnep8u^{{zEcq;)N*Sy?Ou=+VxbPQ zC3X{E-9}?aA<#??t(L)e&L{S<*0LL(u*r{tf5-hB;fD6Amd*%6agFoG(BOTDpn7V` ziRhy;F0^5|mRFp}*Rn(v@TVt@+|(r;f(X6*;Rrue&Nycr-*E)99dF~E6Cb0@2I!jz z)MB_Y{1wH)I*3Cs^9zXnDMd>ucZ2!`SbiKIt;0`AhLOGwqpB`tS6z46{JMmSxib1A z1V1Q(idI^idnNB8&xVdW*60*q()(JYm?O0@w5upV{!aigqR%<>G%m@~$5pQT(`1nt zb?8X{G)Wk7_M}wzSKQ@d4b;ZVTFhMMHwsSp5p-cp+41QF-2VD^`8dfNA~`kt3Pi&j zO*C$hzdmTsl|cG5{GH`Iuca5%a)HO6Ekty=x}H^J2o^}vmBZc254w~b)y^^dL*;Pl z=&{%u@DKo}xv@nHDw78K#=juxa1xb{UzUdubX<3C75!8NUCMj`)R@o3W_bqwNCC53 zwMD?`h9-))vrMohnfEdW_bp`PQ*iet<4F&_aHK4L4bPyKLW=9~EH*~V^}C}1h2lRO z^k4%Ti!bt!lw!CgQHrprP>|DVKQOm*49(43OmJuCoSlK(E=bG=tvMH89ZccC?bH{~ zNYX$CIf+vH&}>Gk^n1E>Qvi`bUq^oM*0)>fl3zp&_Un127JV{49hl-4Mah2_Q~Z=L zB3F1ek^5liWvKf1xEa4`gYI3VuGR&sKt>wX-f?d5Iis8t-O6Up{T4vQ`bfJBA)VIG zx$a?>|JM~E!4ZTj6Cusw)mISuORJZ83#s42bnr!AIA`f^{X+rJpaP8t1}3TXN1UCt z@HP0C;Wqvy$*1_r`xQD^vR+>j;*}Mwj<8qyA22A30n8`l`T${TTnsd=V*7N;A^lk* zW5w+?p#B%{soI>664#x0@pVtOp?`t*Ie`vV)TKPM+HPHm<^VKGpl)p`p5WEPAY)q~ z85{vvAEpVI{VceyPmD-Z{0xz&yo<&~U`ovus5x&YxayuWhrL9E2gn4V8SCW>WFBf3 zHko!?n5$-58C&`ycfvH&j@8mVHfP~jq9M&_br@uE_F zZzmG4RoZ@D$R!8b`XN@!0HaDqB;gobej3K`? zHz04G$Ub~Ni$Yqfvr0#mUO3RbVSf)cp*4+@(R)}+U_zP?zOqm#F8{}PQYV^c#^Z_M zac_$PLSV;wMPV{n1u6toV8YpO%$WM1M`d%Q_&#Grqq*OrAkDzHA4K<wmqd=CCZ7MctZem{1PH9B&%~UGd(e@xPNrK%`*LgFB zO!DCbU-P^S3+dH+02>RKCrDgIo(0y4A^s$>S=F#m_D?sz_|7OfoG95^aGnxp#f=`B&%|DiYArrX{&J0BD5$|BV+ znL5M<6QW@=&k?XJ`mX9@2Ms1*;tNg_aJyOa^NEr;w-fabxTSbn3-sidn#o|OfI7o* z*bi|DEq7q$L)mgml(GPn;bHc7Gjpbvy`!;%c_)U%N-AjhS{`YWIzD8rToKFX*ec9h z*(g|?YYhwGv{TC5H!^c{XaF?)p#XjcCJe!D8@)=!CKdBHJWd3n${}C|CcY!uaY)bD zk~S-3_gzA1quQXnq>@({`CW|P%lx)ic-6i5!&Lc`zp{QT>BN}vuGRzV09>>T@S0o^ zu4=OWVR<)*{?d-@nTfpkF%)Pfn~d8Z`xYiQB<*kI4V@&rOv6{7-|<<&G9sf+_Gi3I zjx@VmD+v}8O)VneZUhtk^%7;SWq?A^poU}?+;rM6Ruz0@-|&QKYw|{c zbT7whw(r4;r>K8(LDj~wQddM7aguMi={tpNMZoy{hDzJrJ*VUWa98K-QVHtH5ej!WF-Pjll}- zhd}o!=ooZ+E0n8f#jQ(4_;}(vyT$L(L$8zaHq`sIq!VQm;!Uj)t-x)g?(GF)wgo%T zAu|>WXV|8N%BFK=`m~mItB$I;aOW>3a~5~u0RORzFJ)lj&|RWx8#hno$G+sct5^Q) z(e1KEg+|>#Co?no@=-ISA2Ykd zhC?84O8T%4!mAwFsb_MfD9vhfgzFenjO`X(9-?`#DSDYWb@7Zow@#zs4xtYgu-9=7 znsuN_X0F@Ko=ryjM`QgaAF3hLWjDxEF&zIXcD-DU{ljos-Dr38&K*9CGW%??fHB{_ zQ??6W|9Wmb;CMoTK-dR*s@%oKPty*rwJT78Tb@h^F3aNSZzrl6@_Ww4-7msqV!r!F z6(0;JQQZJ8C%4tov@z}r`A|9T(~CXjQAb7Lk?c16yvE*S_u~9Cz!^MhST^7yy>+d8 z!yrB!#3de!;eFaC;J&I2bXF~>{2Lk}OfwzBf6S*kU_C((xx-A|u$cV;JRY`~tLV<` zI}G3h4PJ6}*HI!E8-(mboOmk@wpT&Z*XyH0E?a-xo%D;=gu{NOX0DsCbAO2k9%?9i zxUQVKvhlN1q^Z8%G7~Id0QgaxGo5M4guUR)x6(Z|&pQ(t;Y1xkjTXKeP{>%IqW#iF zJ3*g@2pc-xwf2${sVE1!iI!{yd2jyZ-5-c@YluTnbUp|PUGi$|W5i+U7U7&g!IMUs zh@8|R%`2o1{cij%6*!) zg!KhgXEa!Y`6RrLV%>IoKZ`nGSZWTQVej!@3PZVq3(>9_s{^~*E8LF`w&oq%Ae@HVj`UWbUBhNmw8`a9AOjlqo?G&N=qbV#dyHhwc%_yaD znYRn2J7*r>ydBa2G5TXTWhc5VvKzu z)LAsXnhIMx5M5ZQZ*es=_MZB_=8Qm>4q7(uTaU&ToFRBWoI)bpID9(~Jg1e?4uY@1 z2hTk(e$fx+F~;3f(OFoAL!1E{uB92Wk=V*%RE_LHP3%MRk|sf--%*_3rKqhYZwRAt zOi_Y?aK`N5I3}<<(|HLI^CgcY(0-a)x_3o~^D!jivL^_-VO+uGNtw&P8@##0rD+XG z9_TQ2b^b@$^B3fYJL}^7&&xw6;B2&)5dvav$}kn@=Ru>qy836u-)`8bTY80Pa{$fj zSqv-8g*p0cy_yWPH3-)+a$obGCOEYC?)s~%ILk@Ky%ZO5>;IGa0Lp_hIA|Z9+ucpI z7^byM2>VEVm`#{E_YC4%LK*<){L!s+7z9B&%n}4wK%zhB&l}HUX`Agz&MJKeOnbV* zu?ig1u9lttL}S4hc)mf{%8+X5zSIpZC|)W;Zcheh%aBY-O7vR?4X@Kqnzo)X6i%O= z8*ft)GolCq78mGTHfpq--MEQ%4PLVkHCDaxrvgE!XKFdzlw%Umvw{Rw27hpRi=b2Q zqS0AbV;Il&1UqsJh5-Cj&CUv+)5qeY^+zmag#3QRb=4RHGAgM;@eBORa?%u`pc|zo zfiiJ%=QNo6$MSe9hU@G@F57y*JEE@m`%srvU;I28Lx)a~if}|}e6hOWYs z!I@a+z%$bJ)hmu*eYM+Xtr3AmC=l^;->=gYLRN*19v1q$3WgmFb&6*#h9#moKD$2= z%|Uov73j?>QI2BV(W!BbI_nrXstmJ*!Cqug@!=^q7Rlkn;Yhb*Vv&g66ZF%X0o91E zX4Gj%c;r80E5{fK&O^%aUyG+H4Lu*DnNrlJ=YM)9%u5gXwc659g*pvn?rgL&E;Kvo zG!VjeiefgqEesC-OrjjDbiVN+=R8Z^vG9^R$*qc|O<^ion;x?%F3z+0)H#}iFKc9= zC7kJdYK5v-Rk3RlcK79CYpO&%1dp1n3sD&frIHFK!!?kr!QftKIw>;AP%|~3L z^5P5<#so(fb$?y|UFC1@{^XwHq)i$@a9B!HNB~heu-L}QxWOMp%`r<5B$|D_as_NQ zvY>l*cs~}+xsu;FbX01g1a!@qxS76a12;AXuzx#i$(Pe6;)Zm^V1_G(`MhUtu?7~( z1KOEs)@H&5EzAMA^)he{Vubz2sT*C@1z9Hj=Q=8{s4LiHj~QW@M;=)usn86WJJ!6) ziV}?h-)Ywmi`_%cUU*e25<`&Cp)kCu<*X@>hNlf@(a8=OHohle7;%K6PNmaj^iTyh zCq^t+`gpK>@Ovlhp|y<}Zt?M--NM({R4H}^QY0a*K%m7xvF`5oaBS8x)kK`GQJ{Mi z8cB8hq9QS6vb`t{mBhYz>os4?;vQpW1Mk?`lOM_uFa zAyP*u9%ZBJuL9*+F}?kNI5`ZlpKaNPRl3nql=UtTM%A?^vmc#!7Q@DRg4(PBMI~44?)fr~VXHxuC$M?qj9( zJDsG}aE$Pwy5Bh{3s`jyivI*|s8*y%$~y zZ+GWegL~g$u=B2uUWbWv886!rz$iu2)Nx}O%KJLA;nN4D0!hFM9YUAmgj&UR9~4Sq zvR!VgD&b?;FHpP?J|~{kh`9l2hC9+5oQ$xLdqbIQxazs``!lctUhOyV+@#0om2+2C z%qSH$@y0I z3_`7U+PE%S@qybLRdv^5%LWQmZw$=VeU@1BSvkl|g>H2TH&DKoR^j zDgBjfCTI0CbKk`7&I7q9q|t7anpl6J)+N>*d)jU{vZ(s=K`yJWr)}pNcpp$eRQN*>UCT z_j5HT`l_1}+2=<-(+9tjSvA$W;j(Ml zE_y4lANYYV^eQN$0|*-mLa00S48*kQ^1p?sn_F0AV&f^lQAL0xFrGn>Gv5~}Qn{PJ$?{Z+K_v_rSO%o4UY?l>f;1=W)Z}htm7$PjX-f`xZL0u>@_mr3 zzX9|1+-2&Xh2MikI`3P*hb02;oFVDs6KS-2oDao(AEprauPORjg-`h(5;B8lDC?NP4Lm7#t1z?&3faPyAPA>kyUq9e~I4 zgG(knizd!KSO3}l6s0?zHFWAQ`35E633re;KGSemz_$KyJKHC`A-;#F5YM&m)ltAz z%Soj*GZ?Q9D`*cv}m~Gs#>5^^xu3tvj3b3xc zBb4dOS>ZtJvrVk)CTjTs$3?*v%h_fAwz4xkB_YG4y<5-A>I`B2{FC8B{v4HQ-Jmw4 z^D2&lfsBA9ZQuXAfR@^M8IB^MM2LxFZDArOF6@5&BiPZzLSJ8$;G9e`0cf(#Cl9q+6nNx(` zudSwL`{UbVd`XQCJYH-fe!HsKM&k^-6KBp{3V+R+D9TdnVPU7{VrX z#7`1wg|dUl3Zz=>j(O*eiEy5K7LaP>U@$t8JrE2px03&JfM2UalXsm?z!x-ONyb!e znJq*}HiY7=g4gfvox(6KS8h*bz0m*qtRKi*_UIta6-9M8|2r0#U9CGrVRybe@_|Es zv|-BB(q!_Suwa>g<6u>b0aPRZn<8fCjT$FNx7b9*LBmaeG6o3dI#QXm`9R`eMsSeG zoDIx|W}K7k?oJN}i%O$=X!!9Ja$0b#NbRMB*($cUP1>zW6^jTs7ZIOS5NYeb-4MqQ z%I&y5)sh||A*d8{orqqn7Ht3V`zMp9yVx9RA}O`LJQfb%F077m^l6xh}A z_;3dkprSor;};~&71%eLh&HNDW7R? z`W0472kA6kHdWEh_lXd`^Tc4EU_+Eq=AH_N3cf{#i1r4T@2WOoEB0VPKDZGPb&8mS zzx;~g3rRveEey#m`=mgm(_;9OwGul@6G1{R@UpV;-Q#6LN1G2aAV*bX*r)_kjy&#? z369$!m}VmLG#w4J{4+zv|1d6ih}OV(kSSAGTDT_RDX>=4wI3bMl4|TZW0AV6sOIw?{E_vr z+3D)2!qMNh73(61#`Dr+$^Yt82jViOM&A>{c^r?_8!e~_%>Zp^<79;cu^YTPc67sR z9D)o0pC9P`s9cpgWPd4&mJ-QqZtY}esjQU7v8m$VjJ%teeL1E`Q+7t0i5n(J~J zlBR=nM)b-l4E5?&<1#23~II4%z7^Q7p(bh z%hEoJi)~|9W(}2H*!KEMdVn1RI@I4J>56Nn96fPsJ^jvogbQ)N!072EHJWcoTjsxZ zM*3hy2Vag@z)uKRF{YzBp`>cS>*U^`?+}2=;G8tZE;n5Segtfpf;KqTo@3&7j6V)N>i%}8H*RPfwvre@H4zs4K zhq)?RISl+;}ouFtZU<*q)p5E8@n!#(jtCD)=h?(UoS?lFO>xVw6UBSX~koP0N-vYwtg zv5YCkg5hj3#!K$p0r~rp#Bjg(s;78II7k$?0h9KYGo}>|5`qyp>aHz^Ql5W!xE*);f&1a?rPph_+h5DygP68| zE9lU|l@`-p-&Dra5-(YNyYBYFYd?E$Nz}eKq4~7^8j&ilCh3HrCj?92<~)I32sXH4 z+^B4=`s`gYMelyBoyB7;nj~L^=N2rdA2wd6rxtpcXUH0a=>g4eaaj1iYcW4Gwx+-P zm4dFzP;IRxNZm zm$BNqu=U13_si?VZ_c*%Ln9l}42fSgg;R}w`!=LgS!Y8BtBi^zCVMx;vjaI+p>!N} z96oWCr9;B-Pr<)qak3pN)}Y)b{OtY}zAKeoPI(Wtny}1xVA5dlJG3zt;iTIDgEkBo zB$^#KGr_3(5+oYT|K$%a<+o4S&{b<-aRAN(mvomO{W_W_h-eT6?OXUaggfV?=Y3fF zTdfM>HK{T*5*V4E6;5aG1s-nPsXM-8eT{INy`DB(-#F(q#a2-GEJf8Dt>BM;f~Uaj zMx}Br66g7NaeO-ZkN{gKOmTf8{qbs?z zmcvH(#0jNG;cNHCp7bK|mE2&VXeo*iv)e+kf~@B(Cse8|e66Zn3*>KJsjrG;sf$(# z!=tOh^r<#$cr%DJ$r@A6kYcjTXG?5JN8->qq3pWl7AC&QGnwOiNIw1EW7ej)I;wZW zoD*V|$Kae&c6bd;H7DP zcURt`G~j#tygU?T9E_Igc?qO2xCa03_1Y z)*Xp&l%rk7x5+-H`=XixqM$B+j@HERB}eR~jS3Qse0_=6#hlRTZrkPAe3F0UDP5ha zzRIAC|JFSus#R5pIwJE}={AhOk0d#8&%o5MG-eM4%r@CX;L~OImOse*yH;0TeHY>D zjtZX#Y&+=Ifl3cRJ!{+A?aZl+y|-iD~0( zXe*MYq7(_otJfS4_c7`q74PQMb?^;K>@jw)S2hPuGsbQ0V{w*@Ank>+i;DnukLhVw z-`{g_gih0!!(GRQQ(efN($d8ZHV444#ok(LHv|ey8~*3xgoq4H-HbYkbQD7xpkpZ8 zEZAUCKtPac7{CX~V6cD$Va5IN;HOH72o1tb6C+!39T6$!QHoN#NwcNFNR~F-jXS6` zG4```%@K7na6$=Vnw8T)tYo9(aGH2ob{eF-i%5Xlhs3HOFmnSRZv4;n&i#Z2zy%)` z16nDJXC0?sg<*oh4I2y#AQWsNvSXb4ii*|Cg@z3GCET7Tel zgCeR5z+4l{2#)eMAh2v%d;k#ym{44f=U!Fd=$GP@`ANj9i7Dl#J0)adQtxtZO^b6P zD*dVff3c9zF2ZK#O8&rgQF((SRpAH=zawXH?K!!aK1+o&+eTZK=QaTo^`iPCcDI)# zD;M7%!CPdf=Nqx{M+4X~%Qvzr3uQ4b9m`j@uB9K4ZpAJ}d-qj7Cw%b&K_jXe32yDj#nHJ6tqa?ylz75Yj_C(8qQsYNtsw zx-6&eY*-eT7(U(87MP7cC^Hby!q5u%PQihj77-)=%Cjio`2d3UI0fujkdqfG5321B z)_leuMqEn)4Il~7cYHI7yVvqwGYwcroiV}+@Ckk;mw<2IMc=BtWbul_$;Sjsw7~sv zcT0*5JtdrY-~O~JV9JxRoF9Zde-Z#D+9XFM(agID|3F=#tgz!hPqlQTB?iKcVKODmd?=u=rq4Bz z!D^#FUfTgx0PS_S_SG&qjA-?}64y^1LKA(AcFFsVI+UBvw>Bu;{`|Dbn-c{XBV_uj z=kdaztoH2HcYvoL0N{PMX1|1p%d7&+X}@1txtnSP8orR>}(sC@6Q? z42jAMs(MA~^;9n6U(i8W3UKpOBuyS!A3T&--1Dncu4e%4Iq`QFK7zXfdKPt>#urwT zJv&q|5sDFqn|ntM4`UQ^9_J}e*@VS8H0wMPsw0o zkzqDm_!Psd600*IOGb|*?ThU+%%c|@UexiSbk#4D--3CH>Mj_CNv>;fYwrbgH22U*YA-n4K7Zch`Umam879%QBd~{l1hb3cD@6Q5?le~8DXl&+-MT)4 zCHgZaCHcW>(=GO4DS3WEgB|g8rS8{S8{t@GIED7?^4;pQFPg9fgr|;HruL{9p0^XS z{0%MR?$D5^nj}b;bZoNt>#gp~ePUPQWAxplt#J0*@YC7OV^C!k_CsWKkEa7OEKJET!(OqjRdu*%|i=ye>8Jy8kFy_YXNcg@$2h;haG8(8o>t6r2zccTH9wYx|pyuZ8BpZX`f5L5F>-_QAqu;KAtfg0m#1}*(Y zESOXZo%VNWr{8rWb8mxeDU9l$zrSsB-#XlAu79mSelUM8{F?&DlPw=%Fp4$FB=)xC z(^R@)jetced{yG1Qe(Sg5`DV^d_=3#7`^3kPV7^kG>}hSw{EWU&xr6X*&l#45p%o2 z#YL6YQtlUFF%-4dT8&xrZ{rlf;Xl8|aJ4u3AQ9E1mP*`P!A<1sv4`zhL{&9DfDqzCI9SDMzJb;N?`Xi#f0B zZuH|8P^nQL#BFaRGi3G+3oiwl3e9=oG0+FJJB!Ss`sqzRvSNyi?|IRt`$*0gIZ4u$ z`c22i&@Nt={;dws*vyVuLE6T69vG<8PSvc@@TR+kBN{Ciq;t6??4(^oRfKVtobOcx zjgPgpt~d7>{jUi3(*fqI0~7B)-(EJuv4)6mVpv7W-1IU{@bBHGczZXafdTr09-t!G zuL0*{3K7(-gCT;>|GIrviE_zIu$b}U8#)jN@2GB?T4Sqypg~>)4LL)D<0PF;8|cK^lmyU1;x8EN1ffL-z-wEt8*Rg)(!Ib#(YCW_;L8d6VAVSthc3hz4^WS5{j>O*A}7MLZ=EA z_cc_jgTunVU_58K_fuJ6)Ky(K63xE*@Un~UI`g`9QjYXv^9@bw!1$Mx7=P7(}PE%v7$CqPE=s0gsy`MeEb5I17 zh=ts>p0O^tRcXIBd{-o!8reSdE)$I5n(EEV)rQ*F;^f% zrtz(e1`oM9a|3#ckWSjnjU^Map4!|tA@;Y)!t0(?N?iOxyPpBj{v4Cc9IF!=-!OvKf5r5w7hq$l z>vsW)x!R(fzv#s<%AtNzbfZxRPsAKY*;m=Ik2`T7{1K=@=iMd}WYZ z4~eC?@em6g=jM^N+w(8hKp1_`1#Zk(|9N>?U7?R!`!nB=Yveva0)UndG;~D0?HIEDe3pnDh!z&8XPqvyn zkRVx;UV!M6Z|~^@jV=o@6;=UK)F!NXBcYl^O0CmKf795eH@;7z%RP!Hm~a(d1c&p| zk*K9Bd;D8?iTXq2JF$^jmyytS7gJatUcBy{H1!VyAJb_J2!1^VA5yiABvEf9Ti?S>eAdaJVq@s| z0y9oo*m{N>p5utGRzL1*i0+sdddZIRy)T<2{ckf(JRSRcmsx@jMr_TP=Y zF$ymx;v$N=#(0| z8}9Lc?|lyg&p9)5_IIzfK8vc2arvsQK4L!nYm&sLwtr`o_5Le?8%Op)ltQ?HD%Eaf`x zR&`Y|;8SaR)3oQRMz;dIdD}pjS)$PC@5<~@lJ9a>#i}WJ;j#_X7OG#8 zkVntdp5GDftv*^iMXe-G=HHnjGecoiE2sZLNNnY%AOEEoFd;Wmt)0{mg7F67%qd-v z&xPNfS&bYwM&O&FZ=3o;kSD(Ys>tV`)6ML~CyHZ}%^FIsA9yI`$}kC$I)kY;(!WIh zP>;U~>>0ps$fAPb6YZFVVn!u@$Kzk~#m7&TGi5!*l70+hMZf7Ttun5Y4ae;atRsrL z5m0hUowTm(N(OQUYl}Wgqo*!!ESEvwV#-}t(gtu<<%&7g| z=jk$V!v)o;Jw>R&UrhED(=QT^QyqqgJs4mz-4OZ56I)ONW-a?gPT&d8K~QCxbI!S0 zU1R8Mi0_AbG82s_X>u(ZcTLcu*I6~;A<%%7o`8(Oo;ykX$`6ik*VemK z*m%3H5lW)NXDuhxr=tA7N_mG=e>b z$R>(O3HYh}`s4Q%9jDaKS|hpTg9J`HVEk_zuet=ZvF=ZplyGd<&(=}P@G_uWks@Me zNX$MQl4WhLya%O|RPe;LiWIg(eN@dus%d@M5>B#%U{P;s{+fJScg4|s`$16Lsx|(` zjoONlUk~NdvGz?c3fs{kfo`C2VUspn3X+GxHb*Q(KjXiwAhGj{(bn0d<@!PbxB0+* zTD*XW5HD)p#HO6#ee;7MW0=hbw_an-k5VK`H0nzN*k9b z3mr++^D!d8koIAqZc9ZUjk^(5s=oj1<(X8$DVk>wBI;b+*J0glm2;a*I2&i2IKnY% zz$=VYB&`ohI0e+8SME=nKngG6x7Btx*9=1-)`yP^Sq8;zc%vF^Q)<`C@A9k*FT}1~ z%_y|%DfikFv-oxN)Q?@(gJwRngzS`9U9vS#eHgU*X*z`5exqyInjXmA8 zU58h+w;Q~9g(r}QfUzrHhIsuj#QT%aM)vA$@qYK_w1L`2onkjxkQ+B=m$~1x7dHo0 zg5ggG3@SW>UgR%X>wb9Um7={#3t94?I^KpI1KmiujMdWa<7B z5;Ku#%{S%2bS0g?eXm1+3vvmuGH(BxGF&!2zhlO3SxAfFwj1s_^5tlcG;|9R5G<1A zvEp!N`wlfmBD?8Qg4=NOtIuYTb0xA5dUgp_r8c30X)aGzOcg!m$!JUSDmn3b?^!Y} zKa$wGL7yL<9L1F*QWtYRCteo`*` zj$v3k&Lmz9rP1YSR(B}XR-`E?`dI8rM@`ZW^h)tOiqQgy&}L|O-d`&h;-zlj z5c+odxATQbrwIznB}!YYV*1rK25uXXF{cF#j_0zcXQG+1XW3z<(^Z}s+|U1eDB>WUmYwCQXbJDR!0}>~9)Z0Ta4~GEp@4*3gG%sI!pSG?Y7lSg z^UXJwKj$XkM1$$NMnE@>Cells*9kyUz!@<^onaCz40?mw>)n{d*J&<+^KHEx)~LH{ z40v3vaBsaVvreO^i-=n|!cXxS5*?zpKWEgoAPdY&+Wte}~D`6JD%G}?D? zEVz@)grbA1(Xzw^vuWS^fD*+M;e@PF4b@T@lvV<{LC+@yba%DyOA_VEYENVEyX*&<6M zm!-2fSEH}-$jDQY)GEvx*y_8Y>6PGMvY>rI&#W7@tKJ?1&!*RD`}12aA;%kz;}8n4 zqLSr{gjPpEan`6H&CnicEVlt1iaq;6Vvnw; z3%%ONz>eb*Gr*Zm3v53?jVM&L6G#cGOw*zsrsKC>@mQO3TApV(v&|?)!^=7QDuQ~^ zu_H{1@&Y?cLuUWH@F=`_326}A#%J5Qv+g;Kde@=Qr581JO7ZQY!&i#!80W6Q3eHcr zxna+(?`8F+`kDa!=H?})jSgG0fa+E3 zk-{mNHXSVj8_jE!FPZPZ*}v3+Ss{Gwh4DfQ*4vNhaoJ6VU_&Z}L=)m~gIaYZL#8E* zz!4!^X!KV2vRDcCoD$e5bE3k{uBPY7^&GvE<%6kU{5JzKn{@9%o|Cs2%jY=+4pg8M zr0Y=kjnYk>?v6OOU%N|=E5bj-hqD}6FHvr>Q}z=;Z3qh2j?a+{P)rFiP&7nw)lW3@ z7F+)|Ji3^GJL}g`DiR(|4+1TDic8capP!7~YsDk^w6!-a0p3_vRK1FfLVKcU9+jU) zK^&;RF0bZ4>_G|AHK#ZOWk*rnCXQwxyKy=QrCt|!fxXP&I;yy2EpaSszUH1h3DKGM ztz=a-3{8Nb)%PkIb#j;-o!vi}i9hpp*yPT&PYlR{Y_1uHjyWY)>^^0jdfq*( z1%QqgyK`c7i<{WQu_wW8Ze%@=P10pQDQjblqs+0HRd-JidNb-Lq%__X&spGqk$nC= zJfHE<%8**xoz^u_%U-KZW|b?s(Z(<7N_R^k-bqFso#IA?pzaYevq0*uBtgd; zHdu&LMqpXwHn4VJ`Ap$^$mt5QrTFp|wh(eP{n{%fERP%MqPPE}Ca$L>SsE*55WZ!H zwT`0&S^^IQ{f*v>SD&<5vb*0ZLtk!~%wxh^X-!dVETE5u}{v~ z%K^P2-VK%swI|VXL&8oS-lPGEomipnE-DQtnB=a2tVh6tGmL&_myZXS(OD?gKk~WYhLGbR$J%cBW2+Pl|_7%4bU~lYh(g-R2~0vw)kA7nl$PSW#L`c zEgeXG-ktR~nO8r7KZWwwH4SUq%CL{X#HPUMI+|ofPKt zP2H0;Fx7{mL+I4MiT}24&@O0HX{mv6dSeQld<@WOKitx#>THR;qIM7o*W3T>QX_+m zQjT_>akGff8HKMHv@jhnqbUZ|+47D%`+XTCbz*dDuQh%CSOdRaS;VYyldfkZwsgNLCKCJ3kdV; z4 z^t_BB$NVtv89Z%)&a!zYqWW!H#OL^pz9*ovvVLM1^$`_Hcbm7_Zrv9}g>%b~ebrH# zbVGt03Qpgx0^2mfSH5m|yp!{RM#lAYL02ze7}amP7&$A3mF78?4N^_< znaa8MmaphW{#Ai5dqE@_@#e#P4PYs?}?2h02zjkQ;N-7rCp2hQ(0MyU6@b*dqd9 zKSy?gJ%8{f_&JoAZb&N}1%bt8ol{N_8~YoBP4Vjm!ntcRTj`s2 z>A^h3xM@O`S37sFSK|^+WuN)oq=!amx!9_+CDZ+u^E)I3s)iC0f$@EN9snBzhT=k^J~9E!H($A!-t!C zB!am^PCeMS{Zj1uid)X-!Su&~fAbC*e(4Ob;UcH{?Di2y{8P@Z6iuY{4DGAbn~@tK zy7S%wI1vS$(%GvhY3f+a*z^Ym$Q0gxl|VPL1M^Hz{JvsJ9b^G^CS{-tbP4F|{HX*m zeBI!v=*_4j`fYmMTgQEL-itFX#^w1A@><=^vmU34AvmE1v5g>$Ga$|}`!+ag>!zOc zE@HE_us8X$t&N(u?>Z^t?ih8MD;LsQDZ77l+l)-D$m5dbPvm*|)WAPpF(r>c``b_x zaqMEtKQ-!I7`2&DGt>fH$p@k!;5DIFR=FPEv^)Nxiu9HN8!NUI0^swg_GbE{=sn-T zyX`wBq$$DlYXVS2d(ek9Qg#|umxef@pQcu{`cyFCH+Ui^o0GMimZc|Zg3GOutV$T+ zlV+WJGfZeYj3f~;VT(x8c(aAN+Hu>{{I~KxNn))oZz=1_U|+Y|{rWTD@q(BO7*j6Z zT{mDGjmuM~1aDn8*jDc)>10OK&;v85V6eYG1s$m%GeVKl#*DLyPVQZ z=eT2BNk&V>YtcwR;q(G|foKk2kRp1kNZOAWa|^`ZH+8R+FBDB{PTpPi3aZ^H0W}~p zTJH*)(E`#a?8sfKT{N&2HZF+J0Zj5i2?9ng8%jV!2%`HlW3veQm^E_mt8};-Mm&;uE`0AL84~41iM_bx2z08t1-g8=ke&qz_ zYeEt`SdiuCPU6@%%~c9OIYYaRb+>4(o)MT5@m68Ud|@}&hRI=j@K)v}gWVF51jOJQ z{H~lLi|xnwR!=$Dn?A2z2_&mlVuH0=?Q$ZtV~?Ft*_Bl1hepJx5GbdG8TSs;2dm^# z&;vSj^H$FSjc5VM1>FU(uaJyG$?~TRi1(7mJdKlr1g_=`?}sIzttUZj;n-7{bQVxd zx>9ESWMu3V9PY_+w5%ZY02sR{+Qbrjg@3f9so&7#1%R)*!tsFe{TwYzj;QpfO3R|w z$k=ngM3-y-#b{!P4yy?wpU4QT&qlw;&9U=I=U+9+^vYqztu*)AYW?DGiOtNv8zsWg z{(+PW=}uUDY#ArlVQvgDV@Pls(|HY$*V>+yx~5~va{gFhTIqbj!$vQ7gBC_PV@GpH zD|!G#g%$CA>c+)b^vdMAbRz*Q&)6|w#`te&^~?1OXe9sRg*ilXmukVBX1=YsZq9-Y zCsj=&&0h8-xm9+7!q0U#JLKxnfCIB!IfJEG=veR*c@9et_JM)Or#{N|+k7@PzP)cE zAFxXZ<0?yBuWTPHnT(3*-dw?m-h1m-=#Vd}HcV`9tOlnDgXRE>CD-%0Sh@)FX)mA^ z7+q%0r{RSaTsDq*m3L{Mu_8$R@yxn=zq9%J3Q7BX`=cXiA=M*^p@1tMJ;8vRq*YuU zvej-I5(yaKP4@Kyl4C**sc%6}2kD)a(VSMzgmI6*hZLBN-j(K1#~Y{0A50m1e6ui( z-z=*~@Q6zLcSA`?&fvx2>$qzvuQ*I)UG(3zj5ri$qQaOZx3cZBdtp%bv%fA>Y|;Cy zX&-;zleT(Eq6_LLioI=pI2&Sm`cCAb){Y->@1I+TEtstSm)U!I6mIzH3rb4RX{|X^ z;#eTc@g8>N-!FFz?ioQubQ0fN{5Kqa}BTgLmb^}j2l{SQr(9ji6dG6srIy~ zrAL&|0{EQ=A%_Di6^@RJLdhwEnV2R$#t{d(32eU{$6cD9>Q+XRW+I9Aa=#@H4LVIG zT4d0XCSbhL(OQ!PKgM&wio&P=#U3{ERT1_>65OVg7=$;-kFV*!X1rLdC!r+bFg2+; zw-+)%{sB-HI5P-%pAQqstytaVYL4C7xFv^CbTSZl=Be3gC}Ep8|2D_9txxC_xY#Ah z|G>-&-(k5MxH%FlL zxJ|ltUkP1cNU)`oS^fwDnfYbEx;`5py?C`7P#uqWGC4Iujhu|DahUVg7TF$oeM%-M zX+Fc`btD9Qy*j>4kkPVfb-MHec~U`A5=98wK4q(b9fmn3HV!1+RGlfDmWJ|Gnicw@ zDr68vPpYB+j>HlH)$m4!3W_cq3N{-Vy|>5&SvN>$32XdQwOSht;mYgCbA?|s-Tgt2 z$hUWzAG@-x-YO}lKKUI3;~yNd_qqqwtnk5a$7SSH#An-UoIXR46yR6RtGRn}Qouw>t0R<;av8O}d2RXYJ=LcOEBp!oz=mR=+-C*a4 z=1{3Ko?xX=)p8XIX`wth(J1#JpGb&8w?T8{abtc~xarhc*05WeF<)UBqmQJ{{($N; zev+bND*};WcR<-6RJX@FGqDA%NBFmf4VHlEb733>Gg}Q~WoNL#$+emYKK*rhuaK$f z+svPu>Q(MgrpTbQOah&odV62lXqY^kngqn0>3@r-J%X3;G>tS1R0_6E zid+Sh_9*}B_IJTolo?xBIrHUunPFBpVANg{AoI85J=mEr?QD6lGukZ9%xFcWi#CFy zPOLe1oY`)mU9jVlOlomUiv-l|(sVAMz0-u3yEeO(`M;qr6!$<;2rvX~#5)dpZmrq6 zlj=>Hy%cKad393N&anFav^;iMO=fWmJ~4VpsR59qoV540tA=V=moiEOER3|K+(eYC zhK5RiN2c$sQ4&JpLuu2s^`@c-<~Og~9LB+pD!X;@l1$g#f{TbUAHOvC^*GHIt_9-T1xS zaklet@&X*tQBzB~q|QQCgWWmqZ%I{wfI*VD+5akxPJtUk^x=Um`xz1TO!IeCbE)8% zM>OQqbMMF_4-(nWA$>LHHTjYYq40f^uPlA%TMq?V($s*X7v4Lw^~Z=xoT5%kn!dNw zhSDX}bf-Gi*2GnYGMI}d0A+1QFkug`0@G?IebX8+)`A@X#j}icHh(S=pwY~T9J`Gn zd{ObnFKd?nN6@oN-vUyEzZECItbs%>J%|GXJ6PQQJ6e6iP~o^(5KT^PyoTGSdIdUk z+{D<5Nr_pf21QQU5Ek1kWLx~#Vj3ovv7zl_FFl}1KTnJ8&P&B}JLK?{f4(T4#k!?eS-bgnC9fs!CjDZy2AB%+OW$spXQ8=bQ)g_2f8N-6ZV}Do7Y+vk}<%gpONf zH6yb##Eo@5Rx}pgIki7LO(P!QW%xBI+RPV07npfcDKDOWr2cjK6{Yg6jUoJe_|)E_7S3FJ-8WVolBaYHp_ z5~)d*A2V9>j}l>?KXd(nbt zgn95khhM&P6eYyw{q4^{9;Qqp%v)TD0@w|Esfy$H4}J8(W**~|i6`zUc0lJgICWd^ z#L%Gk1di8NurJcP_Rp_Iyu{)azE*82KgwOss~ig{ZF`%-U7zL0JYPrPdpGU2t{+-! z9I`cQ_Iv)NnHl+#x&KNZT(iTJ9rPx6@d!Xk^i|map_sBGfW0Jr1L9nfGC8OGGR`*_ zZBigT0A>0Rdv*{?m8(jIA6ysmrb`Md?|u;E7(SNO^MIXBfh^VkL=WbjlCRMT#=K~C zeE4`!TO9e9-YWDA6-xftI$R=7-Vzaar8@I?o0O}P%wdaCedFiNh}tagrQE{y&LJkv zB?zO13FVqnRj9qa1W6ME?%JIUj;w5p zPPnvVA>9X~VDSlkXow%(x?iWzVAnyl)y(zzphI`0ka|@Gm%~@16z4y_dSkfIbu{-b zI&KJ7%RkcMsXRUFz0N}i#y358cN>eZ7d2xSc53|}$)brLHi(DjyFdJZ&l6!!FlZUm z0G4p^A$iihslzLC8mtOS5H|G<5Z*5<)o~J$vtw>}d%>ce*o|oYX*();PS{~1Q-Tk@ zVT()XiqiPX7uv5_;fr}etV{Yz_muc zq(^d1xPqEAc38()`XZWbD4eT8Lu~3@XjKu$B{IIh%aIOiw}&wLbE?i#sGr{k-i zd5cxL7IvDgF)Or%TVeaqnJYioV)E9%;qwDI%r=wYM zenN!L_w^_${(#;cA2dL)FrFt#Q99BuNtWOW=GnqyTIBCOkt%j6FlhQa^n@<>=);Y_ES0`;P(A0JA z@5ZR-2(SAZPMOyzbZM+r3ytuR6!xZyFWVtFqp3Rg8gX@U+~DTOG3zPuQGO?H)3YQ1 zx8Zb}+Y=A8F^t>g*K6u4d|#X_e&kkTFN_kzOfh|BLzZkE zNf}o!&xN~G;AfJ?c?@H}#hE{DI4J)_T3`mhcz{F#v0!q5ndp{k+j%+_IJI%32tfZg zJ$$EU?2H7RMzu8hU>8kye;d-o`c0nS^OO42Jt#a3Y(=TMOffCXSDZ%;BVWl0C2`}0 zb5Zq`H3Px>E!dD#nhvnB+7?dM-J=XrCx{$^X>IWICb}BU-5zXTG&Q7&{`4UvTe~3* zG9I!O#I^WL?wK|!awHUonp0lNS&qXOZ?>Rt!yFpC&OJf5;wV=?ym`Ut4F!JN_<4Ts4jvcQE5m^w>^N_F5IA*VYqyWmEn3<_R_egWZde2;JBT(LHFbASMrbrKrBvii5%PL9cb}w84!KSN zA!`CioO;M=SHmn8vtIE!D}_D*TN~vk3i?tLEz?_)p#T0j_Fqnhp8WPL=#1VxOUG9I@TY*&W$ra{XJQl`-&5gZ662q06qWG>I{&2 zy5*V<3e7#*VAC);o1v%zb{|-5Pj6CHFDHGaAp?W(`j9PxgJQ*bi28VcON1M$?$@0- z_=;0QU60WjO2fuXeaJ^LjeN_U(heQgPmRJSQWu4e(TxE{|6;`7=0l7D;nRyTA;6{m ze%VkzOGo4^CMsMg=$~b=GF>^8Alcl4v>Q$(9~MEZu4T z=XM6=HG`WhCb@<-8|TPtKU+4dm&l9~+*KmX;FI|5s`bR!Y&O4EG>e!1NoenxB(__v z)LA+;1CH$rt`+9*bdNx~%hmNfzeJc1egbAeF`wL!rjmnvOh%Ep?wp8ZLcvcO9Sa>e zfo}qEOI~6YM0p(;R@wx8O(JgH{b)zqPeh2X{_mmC9Zl(>la2gC2D9KF>Uw6cFcY#Z z!(RV^?7ANhn9Is{VgPA&niym}B1W+3oBUC~Z7sB|7|BVBmqF24#P;%y8h;C2rn z7wlo+Y&p&C^yWhM3zP-!-(Z7^QwbzR%;!;3O(59sf(;UL8+EvYiLPj7!~ z9MM7b_)mL8SvBe5iTEc%$1M(~xzn+l%SFQE;#&T_iB(*;ZJ93PR7=WrEmsM>;j#bu zIMfdD^!(YvTmL6K6?j8a42SmZXC8S|pp5f%DIvy^kM2$fQ?(fDEXb9~h#zIJ_DpBZ z6H?8S6leb3tbn@$-=tbL)r^tK5a4onVl8mnjd+|owwWHBw(O{_e%5P}@yCX+GpL{w ztOLPZ0s%Alew?$=-Jdb0Z@@oP-?i?t6|ffQMTckIeh|Mc zSMf0Xfxp7VAdbEES?Iz{8hvhNqklH3)XuxvPv<;WFv~z^vb+0A9b@X>5$BuuL^g@O zG=<%NbB;cMVO!-NNWDn(ZAs*u^at10^Yr7m?KYjD;D6FO&lSN~0aajd9^P|4n^*Cx zOfOteAITCK1%Ndlw!iU^1TkgiVCdMQ?zZGp;g7I?F7#_>MO6T&Uguixq&X-G8zI9D zzCmZ1zy5sD8Xk@DdCcb#;NM3~Fp*1yssBj$quS+RWK}%@QI}E&+f4dCUy_=9;UjMK z`N8TNjPkhYQCDD}K#zZO?L#>NAxg=r(dRCU;JIa%ioWu7zUd-vThm`{#oI3v#!UPL z_hNR73kKg+8(9%THLQY=0$2|{J6{m@5QMf;(uFg8^^3R~_M{Vue`8gCt(W)Yg0S(? zTK??RE|Txj4g1mb*D>6OI2v+&&*aKej7xlD_{JN5wt!cxeKW12M{84p!Eyb8#{r46 zTbyu7HvnNdvT$ysiP1S5@=Mg9Un297^(yi;)D-YDH)k0v$zQhrw{@jJlPWbxtP4`i z@2^t1{EoFqFyx2g<#PgMY_W>l5$-K3xsqf@i@V6i=GQ&u2VMfJRFI2(LS6plAj+cy z?Zu8^Wp1c9{_&%-iyL3IC_>3v>9d(hjqNU~*3YZVp2P@`b??Wwwbz@1LW)jFg>CeI z!2StU3AN=h3ww1y`mF_%sRne#b`__lG<@=nEV&}URHz=Fi8g1B$|W+!zm~sIVjgh>VKdZ?rDu3BqR-MnVar_w^jz zZ#WQKWS4!Y9rnOL8NMj{u-^& z3}iQc@pnVlvrix7Orv|6HUG|96!dQ;aTs>xw|w<4b~&+>BqL>t%*#Nb871VESNfE> zC20KBTeTsaGXM8zKq}>#pye~ZyO+eC#I`4EE`twiAyy%)H_397FzQCpvwh3hjnh_y z%C)Eur@}+B^_5UrIMaDO_Y#l>6S@Zxj(?crH@@w$kB*e1cowYg>jszdwpryv^2iA?00|#s3s^Rda)sAT zae1eU^~V(Aq$Mxn2Ul+#zFQnK^N`2wVkHf ziFc!}o0B#^VwIX_`O+89@$m6?ejT zOr$P^ZQ_7F9a-LZpz6yWIlZ>F8NZC(bTV~v2&(wwx^~r(`h?j_S`)6k^P$9V#eTPM z$gOIXbdZ0?!F~Pn>IZF{3Ic8qVa9<7BIf-s!P%dQH6+vTQm_Ii6e)>S6fFLi$pt^-9Jbeu zCC2j2oE^!U8C=iAhnF>)cm6RIBwoWiI&rjoupWCuidBjm|4ephF=(Dm^;8OQP3NNGX8KYVQ?cEi(AOTn#r^4@`FqJ6TT zcwqm5$BpKvBO>qIMmrs=GxN;WDzw^8{-wc?9EP`(hB?1ZA_fawWWtHF<(lVr7RY8= zRB|5|d3O)X1KPB!z3s=^62fLTspFY^+;qJ#bXnw-4a&G1A-oXUb;wdf8FIk_0p=@2 z5SypL68(SrtsnayMu8~;(aYv-M?pr5*V!i~ZZf|7;Jd&3ADMjGs-|K$qu@J~?gY%c z4HYpW_#SR6Q4Rwrw zwisJ3Ho_81I>kMpfR1Xjp{Uy#MjH5zqM)cbPz@lhcv{J*|3)FQD^xk0Y)=yo(0_RX zc|9k`@rI_TIwamIG5J~!-xl~!U2%U49nw=fI*qAAmog6&{*xyJ;#;esS6{$g!;~$g zFM3k*p^?gB`v(Ri-=Vj zA{}#;TD=nT4;8X`j&Al->^IbokSl)_p+ArB{EWm^hTQqV#3a}U2vm`2qIF>}z?rOs zcAx&zbFAHKVgz6`HIQ<{c)R8@YuY|)_Wf#W3w-19@UO?l9AIE$c7BQgj9M}1Q^s@M zg6G3~er6Hxr)t`-A*lzo8niRjJ`5xyKfT)jb*#)N3LO&q$GCsyy4sz6o~dAz*)WHs zyuHwI*=>4i$f@*0wDnurXr-UE9;>Cwpx%a9lgvS8r!uZ;*mDNfq<326o!WT_ek_>i zrn9i^lJ`g>V8u8bg9lPh@i$m{M4?12QboeojbrI`A+!?JS(@Qk`7!VH zz8)HbT0hr&)7n+L%qPd%7*~hxloqe`I|biGfCMPMVh{rE+MfB>43r)|;tX*1l92;k zlF%z(iJ{Y_o@C1uSK>2bmNFRYgPfhJB=C#-UQ~vRz4&@Hvb#nEg%=w-Y-HGq#|otj zMs$6~Qm7!_X=!1)V8Ae!`){8Sx2OR&_6ALY*+qLbAq&A>`S425qBh^3bejbzzjPWFn%|# z-<1>lBkLW?lf)-XYHCc!E?8kVqa1s10;XS}?_9EkKGc5^bGN}amns#7E7sT%A3s~~ z`a!h)w@vG+kXMQBOqqLywMM$$ikr#vvV+fc+VNw$>dph})ciBop$RQn^1@NR&_nf^ zb?VSxae88(nP|%fi0_Tb=Kl3U%b5DTnuPbjv(O5vv|9oQE&85^^qxmm=Tb@24fpq5bL^TFIF!YMu||sV4*XkZ8M)e z&Xa#~SL@KR{)d5+9;Y~rB)!-S6dy5gM960{s2|s*g5=rc%F(ez-+A-CUo{*wCQCRl z;DEYLarO4*U*Hy46d;pKa-;ibik}(-^-L2{WDFy`NMC&KbE8BNioIKUW(k}oH6br- zEc)+fuL>1%L$6>+nzQ8>llXcMxi{CvN+ZlJ8I=l$D;#naNQ1$%D}4UJD45>;+=+2* zetTeUb#aX5@yhh1;nVZ?n3-AU46prL6{!p^i!NG@jRj@L#lYOW!%KDv_!!${YLckwbDS&A6>5mIbGo zu4boFdIc4%4ErK!e9edmO|zFL{H!nup-S!}gI1Q}2*UmM%$X>Ghj(pBa@;jRe%)Pc z*l>I)eWH0MZ$o|dF=jRPAL(n<(*9jH3TI~(?d3)HC@e(Fjl|qarLL`g`niuAhVTpa zwoff1E~!fD5D(}HKCI9ce0#JrX3KebL6*3>?-17-+)3vS#`Wd8hotY}VEslZeH-;h|6rmn zcP#y0f3}oR^g%K&@$`_<9(@b{Ub3ypK)iI_k6PeZ3vnZ-xZn!g+=~>u{2`6S&_X9Q zDbJy^p~=z=Uv=iR`oTdPBRaq2v)GO5SNV@+9$6yx?s$i+3r34tW3}K+Zg3lyj!+qw z)M+Iub4D3k4R0+|L9nUjgL%lC!AN7{X&GcjKlKCVSA^4h+(_>W?j@X;<4sD5Zy8&e72FOz5o>6^Mq9s6f|P{XICR#3!{1vRW9CP&=;x2YEMPg` z)Nq+vO`C7Doo8LkxG6Rd7g7pTrCCQ(OI+ifr|O_=!5H&L&Yp(uIH0oynhqvZ_4tqh zM|`+dKSG^ARcnoW^~5aAKJQPlE}wU)B*{higWHXd zEsXgsgEwE+Gtb_D{=B^$Di_)|BZd8T^tCPsM0lQP^dD?#nx%8EU{};IxAZ7^H*+4I zO`ZLeUdijdd$~y?*{fDhEH>|E-t?%bsZTh zsv6M!ZU&=_49N^|^PmNbFvO6Ch>ql;iaVS4-{j!l5Jw$3v*kOknf=eKWhSH*T75za zH09j4G5sVV{baE3*kL9ID{a?jQo!Bf4 zb+c8*m|Sk)3s_@Le@*okc(+1bQZlszN;WP1;Z!qGm*4nV3SBYu#&VrNaS_6X{AXr~ zkU`f)HP$EN^uwPKxtXx^ zQK%JuZ?3?BqsyWc!u3q8A-2K~e-J600<~^;9RQ#l(c+?k4sWD&<5og%yoTO$0%bX?|(Z}zAlIIC{>EgRtjYPRdJBQY-5VPKg^o2)%J!A}ZXAHOOsSCg$#h5dVH$n-Rh=V_`)eUNp~Gny5)#OvW{d@eyZhvnH&rZ@ zAw7_PlQ?R-=RoZju5b$JS*ShRE-k^VZvB=~e!!jsqr_H_u}36$Gue0hYhohur(aO5)YYH#d*4lU$x{;&;3%#)dzBP#_&Vti zux$r&FHj9AKnaCG^{rgHeb-{=JgWj$&iPhb41nVsjPIG6o8hR^O#$ge2^yu?HVNCY zb33z@q>u?#QW~J18)MkIh}nc%+8!@@c^;hhhjm$6H}Z^bQ$-#Yuuus0b@fOYpPN;R z1Z=I|>sD5pDf!gVd^WX$j^EtD+jT9it^T`8yU{kVT21h1*~$2ZB5rdQOnK;RKIjHX zi3iU;<;{~<^Ql&DRQ4}*^tcNW%GZj=^T zY;-J+V`jw?R$cT?+vb-x8U;@lZU=$Fxn^jKYh{4w*c%}e?86nhwr8)zMpW(UvY6V{ z7gl^MKDm!qOo}{1uZFA}vgbO=FUx1fzRRBTzw|?{H=TG(N#dwGTTb#0-$wV)-{)?x zaCrmSMqZErPq}dhPZ=m_hsUd%;G%l7W1)R{%yIif;&J3&ZGUq`Z#<4`^`oA#MT6`( z;=RRTvT4GlBE<~@X66gtIoz}6{eaxUzPW`ovwD#H^`jPHQQI$H!QJoKCBG+j{vQ^03>Y7h1 zZ0*{f)8SZ(WK^}WxSD-|%R7t4uP$70lymD-IVb%P|H4MSl+2V*^0Lq7R&HYpk3aw5 z9#(>FTswXIke`{7;2e_PrhH7eH`-zEEH8b`*}(Pa2AsanT75prev9hDJ~vwKq}0Ua zt%D1XMVFw0z;cnVZkMTIq`r?QAz~MZIIq=(^OjTX`8YRh{V8nd{LE}5mc_jmNwOuY zl75TQBV8G%wOwDg^7nPJFcFH|qDg*q_DC~iu&km5oeww9^P6gdo;y^Ve%&7y$atlC z$nrR*IJen4V6Te_>m4tkzM56HPJ(`>jrb{j(0hQBybf0xbCCE-2zV~{-@%3YrmRQ9 z)M6CqpNyyTVSk`u+*_8k7o;5tiNl||&7-R@!j~>8kykCF&oe*H=dzS@!b)Tj78GZz zBL}6(zta-8dOL<98Ysj^u~GWxew~|{&!FQptU%46uZ6b_kxD6(?V1*?n@2hUrdl*N z3xsTzJP%psNc?ZXRR=Lt)aJ~3*9rBt-|81bPjmh`#|{d8*qdunb`9Ot zEs}mJDDdNWq5gGpaU!qw)Tx>G;Vrd?s{`(h!=VgQy%BLD9NAyALtA_8c9Mcc()v;@rEuQ&Qm) zRo48;zE@cbV&-H-7XF4ZzHdA#j+*vaF_^lT4pg;OHuu!oJ$IBBaHxjAaV&IZ{lIH2 z$4!YCLBk6>)Fe5M_aW`6R1P--|8TBOMKyj+ybgts? zpu8p~mG^?Fr#ICEJIQmd#;UGWe6Xa$S<}mV{Hwn2i)Wvn*QF}$E$M$SpUF!Y4DKkt zTEZ29qLs9byse=PdD2T2b@8xEUwA5A&{vz(wNxu z!v;|DeeLJ={CF*7b7U}IW#nZ+Xa_zV6*|7q`}ltfyV7td+ps-m#xla#w=kly4WU$H zEMv{S?-ZjkA-j+Y*$G*)Ws71)$d)Bp#@HE(crB@9&)S>ywS@2S{rP@=&)<7F?&G@e z>v_)mxvuLxKgK<6NYSj>jEq2is;7nuBi?m627J-(Y5!K*n#%0);$4J%+rK&v>Um&> zL-c?qTxAjQm4JfckVX~yd1YM&?6|v1%Sty zu|AlNS$u%KSRb{JrBI$*6D0O-g}lg3tAOsa4`JWALN0E=GwRMb)1pB&q7Q}uOgSeE$b z7TyJq?5?j}3(Su=;ixIf?0sSy)~>p_AoMD9|La6^DyWvx=iv2^Z7=GzVdXJGmx|n8 zoNZsZ{-1ko89V|`f>{3+&JHMWwkY96Mz=2$Kmzml5^CnJX?j^3)HdF3RQ1We z|BC0Avh@C6UIGOeB}jan!H+O{bL>NUx7mN_ZoC>L<#{&`G@xeoLm+Hw6_~){(0RB| zeI+2IN-Df(QZZoU)6p``;g!WUH*l^+!EPTQ)d!gymQuQVBm&S5Y1r z-yyQ?VH10!jUzt7E^yPe>9y?c;)NND>}`R2tpT!gw-p%F!N%LP#-MbHUZ4LTHnd<} zVX%x>UT}Z3(A~yb5y~^I=*+thtY*Z7Sa(zssT;k_dZu-#* zR?3^0`$oeReDVm*Ds>yQM4_SW+AG zmQW28^DS?pUb1~wudP$ouR`Y0-5ISdr~RGN#QmLywUkhk0dPP2+lce7+<(X7{S{!iI-4f(?68+I{_CHH`JR z+I&Xw6;kduJMSaPsDGxaWx{Z2B~zgO_wu{`d&)O0-od%ws@n4?bsow^>i(2>L9q7S zgW|$7Z#dos$DCaMXWRs+13x*p#LQ*6?Qo=DH-gT=`2t9{vMJ`TFVRTUZic^Cq&}otS&U-Zl*^-d z(1+4s!c?MdgNOfb3j6e2F;cw#*j17UXsBhs0)xKi$#|Deo=CJz^dQ~Y&y@i*TA$@53rrH6*%b+GU5A_HmC5wFho zp9tece1&E%x0p*x^ZlkC zZil8&QKtFYvxN60aB7yhi3ytLGdu4kSdubswdJZ$9!+B=N<8tQ=lW5-ta!2R_3Khm z)sgV9Dxa3`Kjk?ZQ`b1?LKKMjY5LmC2+2!h%0_#(ZsN>Q!Mq3SlP=Xx5@&8`L-}3|c z-??XWHe=FTuzbFAk1wrRot5KcZK#Ag=zC~$8l@k?tpN&{Qt4rlh?BN?n`@s*Bxl?{Xqa5e0<>mY-5t+oUjh`fxbe6oE(L*?g>ZH_A0!{^Q$4V}PH${&yM zg_rAJnY>5~vkd(*xjED4wy@%>HC!DXLY1`0QWHe7KK)UJKOVg?Zr{b!P@P!ke$OIZ%h+UC?$6UA|HnHQ?zRGZAN3gu2ric$SJ z8jZTR7qR}H=@jvT&^w-TOHYD5pQXyHkss;`l7ogEYiw^i-i)h*mh0nK@l|ULEoC>|{jVZ4u#0FKiF{or#bg2~OBd9g=R^JqzWkZ~y`6%&xRMQslUsoNcS)8f+wrk` zMJ_o~`qahuG&R&=V=oe&EurM_lIUDP>%~TMkDB7Vd-htYbXDOYW2kzpr$lOL$JeLHAbSQpLcn3-i(fCQ?n z>_;25eF*^J_26~sZSyxm0i7^$&Z$An@=JV3wlb*KCiTB5Jp7l*sZINl6+H^Hajoz$ zsnAU8LRT-hE8CaGrhR}H-df=C%Ig_!%34Xfu@q23P>uH8dOc)l2QIj*hK)7Hk{(4Y zV>1)oyI9~>UHU$b;7*&DSLO33awL=8@orzL3U_026R;3eobctXN7IQVHxvReZc>oanXxf$j!wh;UY$~^a$-Sj8SSSgTiHNTwa6Lf(Eh^c+>Z~<2T zD$nMgeu7aji0v8`Gyzl=&)#UPu}=u<*^?hUr`6zove{tc2P7jYrT_r0RIPaaRKACa z0UY9&!1wKC*8(bH>fs}Rc5NWXS|HSN{k7XJI!oSnS4KeCT3ondGs9Hl1!WQ9-Np)M zs=52;8f`b5m=WB9V4Gsj2a0$QS<8f@m3Yep(&r~=)mw~pSeU4cS4TbK8--(5gz0%n zag&ZuDrJl6;!lCE0~a9J@Bvc5Jq1W-aTd~nXEI(#)v`1K^>j+MbyFtv#lO#Q2-(AN z6?>fT3-#3&ZY}7NscdnJs5b8~pW>%k_Jxdket%b*lp#$#?6N0U`vXkvUu-WzOzrdg zFpv`ELr@Hr;dVx85RDvbcw@k-xzok2&q~WLYAvZ5zlqt7Qdu$%%YVPTDN)hq0DR-A z-A2p5WLhlL_hqg-lAq5<7lT;0yZvx$zNhj=mXXmoX(Oc+x_dQ~c>BmO2bG?@mbJu; z@>;Rbi0%ZrVB)JKUz1{TS?24OK*AkW7(z<{JBwzMUwPA*vC>Ds_9>PVeZDcy<(V(9 zK*A9}&q4cyaZa=9RyPMrv{VYBnrG}ma4WLN@BIi_K74t&dL);TIvHSUp=$ zC8~71Q5X6I>VT*T)>ob8=I8q+BcXX`z%M*Ss?I(UStQ?UFFD7cv4-MzYBnOG9L1ly z@pEh7{)lMhD@e<<*7-gsv0axY88MlwZWrCuDTvN#M0yp^fA`to9DS=Bm%%eeyz`K4JYhZm36vE|5L-b|1)p0@fL*>Z&EJNsySF*i1I#!ZTI-rwAe zO$-hasr;SMXtuTMNQ*8Q{X!lYT5)r;U|q8M@cVuL=EW-L$l@w$m+!fFr+eW=ZDAe2;p6+zM1(HM&?NgE2uD>jRCCr#AfT1ei$Gt>x$>Vm8pEeZIf{kNYzKU6ZF zy#`xP<6w-4Icj#%UM$XoM!`EglZxuAgJOWyc1$@dM&!UO+5-&T^V6=-5A*&Gc>eKo z1uaewT_#)fA|*Bke;-Pq&+4x5U~%i@c-7U6RlRCz7L+WYzJT;UDOp zM=UYeh@tx%zUbCVpyX9tlmv+A6< zef!q|pg!}^Rr8~=iXk$0Vc{|j1=h#=HSBoPLFX&)#gg-p?yYP#Xli18vzZLYpi@5- z#;wkgMWMC#B)K?E08$LAIBiup(=0V{D{%e(N0quuG)EtgT6+htv~0{9h;7^#m!U;H z7tfq*%6q=gocqf(W!d){m^(zVO>z;Uh7RL16eQFSM?VOfqOzyan+U~+bmqK=PTYdI zg}lW+fQP5Vi#pgsos+a`z%_5i65Ba^-Shh=ug7jqvedA~hbPcOP1$(d26XB~l1x-x zSrlgnj=05eXmTW7mZSkL1F6>DN5az2uAWKPczn{&<-%(SYv2-tWur6P-s4USy8K59a>d;NEKl9$xB_RJSKkT`{s!{&d`4-dTX%Zwo3YU0dnZOE z^{++Pi~M3cu!=#EAnV%{Nhf?nSaK%&K4Oh%_>ycaw6rn|%B&Zgk6yY^lObE^F(=~Y zr+Hs<_~y+)uKN!T6lY4Kb@?NcO5(~DV0r!^9#tPcq07QVdZXKf9_)64^H%by;bE;O ziz_VP1?Z%jgLv4z`m{B5bpd>7FK|BEkL~4X4RzLossG=9USXET8EbZng3*kk(l9Rd zzF@=2t`SXSax9Yh`Qy=1-KZvGMlJX67nYyefgcD*O#S+pKYy_H^FUsxYcv;Xnep6R z623oK^I_7|}ODXo5E0vv>1XZAW zQ+=9j@-(}eEbA*G)J07RThvCI^QS!OVwnWCij|dnHjR@ilX>n?NqkiFO zod=6L;p}pfMt)VVJqE;$EVD?h8?LaSSm;W-WKaFnF#e^giSm*BJ7r=|bM&P{_o-_h z`_(tXk%<>~Xt0qR!e5hCLO~N~e>Fg3U5*%(k~wl5IbG#X06-M33du?n?a_yxTggC- zfg~ho%j@6GADFfpixOX*dBAMrr6P6stp%`{@kDobyUdQbjGysKXLjJdG&Lw&OtFb= zvNcNJhok?5=IdsDcV75%mhV<~8erW}lS=?|zFZ({UWK zvF96pY5mK$lIB83CQZWVVxPZ$-aramn}D|m&I*$S7@Rv5`;xmO{wmcYTWB!8bWMVM zdDp($`JflmY`k9wUAr8?X*u14f80{@8?}t`^4gl@r|8q>MjG*#h8QN! zW!Ts^N@Gh2upuH86QQ4b(;`2pl(4+*NcX5Rjpm)DL;8!4AlX*_UmhLZmOrZc$kZyZfXk^mzJc=WW5wW>65k^cj~!1WRU diff --git a/Resources/BarTwo.png b/Resources/BarTwo.png deleted file mode 100644 index 7242e4aeace8168b69c9b8d437c8ff719f0586eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28567 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�m6pZ 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 ArT_o{ diff --git a/Resources/LineOne.png b/Resources/LineOne.png deleted file mode 100644 index e17eef4d847d501efcdfe8473e2cc9c923dda44e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39026 zcmZ_01z23o4ls-qDYj7Dt;OBlt+>0pySuv;cPZ{r+}$Zq+!lA&;_z=@x%WQ*_kBCh z?wOrQGMSv2Og72c2zgm?L^vEcFfcI0uM#4PU|`_!p!yvwH0aa!s^A;w^1)0{Mi30F zE)M?101EV-$Vfs_1`N!T3=GUa7!2$Q)Z~8%2Ij&526n6u2F8^J28L;$)uzA$>IgPb z|7t2D14ad^!-7G9e*%L9)xbdo3>*jS4>eE?OcEUTKXpZLihp1rz`(*Rz&`u~qY1kH zc}alYe{lZ3Lgs=0mtr2oztG_Ed656s!S4Q`IfhWGfi55IB{ZDCz~E8;yuraTvarCw zAXqGv)t%L4q`8djZ0HP(?F>!m+->atV1e=4z@@Js`y#D6X#NFb*BiTCrGcC{r>HqZ5Gtx28|KHG@ElmF(Xn%VCM*C}C ze-Fp=hZvW$nApsN25Dgz70|Dyg+*Z&_? z*v{I{QOVxG$b^saAC&)T`XA^&oO8)JT9|-@`bTts!2gf7f8crO|H$=!M}K_}~mrG=G4B34nbS5ma^uKh1{eP#(M+4qI37M0LN7jg0IRGotUUj$pN% z!z!pFr^*=kBs1q{$R6=!$UG3o%DQe)7hLqi2Q>M@pj(*J_xtSjgU;s^pile78PKPP zH+Kr?<#xW7&fLxEG0`;EKm9YXW_5{$6L8Zw==TlQ-u3k7ew>}@n6*Qz+jVvhQH6l z7ys=4$iOqxx_3SKGI0Mdoe5auiue+*)Ja;W!{v>+CI3R4&`UqBCT^PG-1>U7?iiHt zze*`0gaN6+lq>n#rBmB`>ri$x_(xm_FQg4-UR>ed>uvlWCtpnwEFmza)7f%7%KP#hbY6R@@HFye303vk=26-!40SU!Ty$ z269?gnSGV$jmG-w-miH75mlZkY;dwKnNgiBkZX9*t9`Y8aLmt{`CglX9MW)%ZyqbH z4t4{V#a!M%U;@|`lz-_&hcM)8_~GwTE)G^?7rBw66f?BaXw`Ye;C3GW*lc#&5VFT> z{7|;wkBn>_ncLpTz<^=OaJqbM$}?eS$$|Y4k!|!31xtT{a+FQKC3$_GKFo^;K)PO3 z%KM@9`EscyWZ$sCo?U1!0@QsdBCe;fq1)Q!u;2ObT>I0pA>|jke{VMb2l?Nw=?ix`QzxP$G(%eg-(b!h4RNq1zzTO*uWsWvB8cK`2V1)R)Jd*0S zwQ1CwPTtn>I@!$>MhFuYjQ^||G<@RJUapbVQ?n9)T_Yh;j22(=pWgPTOw(y|F3V&z z3N@SlA1Z@_J!pJwY1Gn(hv&fdcF?dM_%XU+6ZXLu9rd++28DbeIuq{p?2v{))ds!^ z_lX?d)t?!x<6&_D0ny+_6Wl}m|8n$=MBg)0!ada7!~5;=pjxZhw%X}na{Z@sJ+QwY z(c6q`%}e$sao%5jV6Eyo(rd|Ie}2M!6A6-yD3Ppd=|7v!@WTXAZ~-H}ExJ!MY#UB@ zar4hO?AYUI2{Fh|Yy_qPeLyvyX7gWt?1}oYJH0*9(*KU{0syzt$*cIaOsmy&%Y{d3 z__X?B&%R@D?@U?#n+KV%3H~H_Yjbi5<~-U%KmNq1s_25`K_r*LAhwax^oDi`7?P2Y5HNp_TbZf#@rN?g9xH-fM&`;Kbdl z8mXWGIyjEFU5n@i(9Ac3jr`JsM@`WEaxHbAnENtt)_&3DTjc5C(VEF%5Xk-ZutO`c z*kGY_f48Xj{Eih}5XZODXfZnkbQs28x9RZ*Kks@zznS`+6v9BB>;2>TxT1!tZ*yx- zN$w7M&(7~z3*9aHlNeR;zuFfpKpE1|lQ(t?{zP`d!XA!Yxmn-&AR)V*G1=f-7tX@_jyYJVR-lN6)PZ!m*ZHDZCE>4 zx{gaBJ7-w8%gx?(^K7s4*7JB_BnK_q)}r>SAzWYp48dWJ_w`q|gKW2_!zsRJYqwq~ z%!dP9+m>G5*W20i?I`-+LqQ7&V1sD?97pmIU|c-ew!>jZ&ypJ_{CkBeN~E>=AaQw# zTLsX44#xn~k<9hJj%Yu~@p{36sfwwn=~{VxTI_axK5NePdVjqK)*AGCH=|$lok*oo z_Gp`|F(L?uA~>wtG-u=T`*wgdcKkE&!uR0yCWiq_Je|YgJG%r7p1uEjwaseNkmi4M z9WsOgzAZFq*%8pUwlG$VojS$V?GB?{7+qxvVpVelC6??TxCX_Iz>C-u!#X2ylN?lG}zV%hB6;Gs)d$BD{3UungK78=WD7p69Ix zR2U=3>U%-oKa4e8uLI)pU^#_ zcZZkL8qNIDTU5HV>lb|7;1MW zQf=+6_VM!@>R(H#_8SrM!ZN9N?CleBr-W!`)R#{zVAF}tdg*V{GQKfdM2{4@BxceO zhVTYfWLFbdQ16Tc&N_8uAHHY8lO{-3e(7~*tcTkXtbi;q-C(qz;`k`?fCYfw!r1F0@__jXq9-UuHagXR)CU#&(rwhlEz0}Cbxo17RA0j&xV0ij~n z1Hh=0=JL5D%Q2Ert{3rr;nSAie+O(myp~QAnSrRo={yr3e1O*Zy7B&Y54@S;zfeaD z&*k%Zau1Ok53HtXtL=K&hQGfar+>y)!$MHa+DT<4#lnn6IN_b-I-%(vxhyS+y&ej` zBeLs$ZE~ZgroP!=pzwRx3VWWv04c%t+#)E#FnrgvRg+Ym4g;eKR9vOe4~nM*|0sxB zzCV+%BvdYf2MRhXF`A@`24cajrLjV7O-4HYt}>Jk!hq6|!h-+Z@^mEUcN_y{z`*LZ zsN;4=8>yz}Gau(Sy{#YeG)(As3T$4nq~`BpbUu{daO}0$FJ+*k(JzD=kfv4bHmW4> z*hR6MUNVIGo=N>5HTr@~(E?t#j~q$JbvrHm8XDSw#DL^XpTBuDW~d$H0Na+1^{!t6 z1Q}6}uhm$n`7Q!#m}9x;wfL?}y)wCcz_X@RTicRs*Xu1KY4grnan+43AK*z%_c;>R zA<^(e;2)V7GLr7lJBRfW?hs>r+F>Plzm)&iCh;Kdxjy}M)TIwc50}CXuGLuk{eky5|Y)hG8uo&F>*YRMBY2D5#?u z!y--{najT8yH&expm-Q@k9;lNhxhLH*Ne`4gC=yK?!?H`Wj~;^mq-=?jT>?ne`p15 zriX^n)O4`Hg4lm8Bbsz7Sq*YjhAbTW^cbhRV>i!9NsZ1n{>#mZYO4knhs*i z+jQ9ZmZFpOP>{}<#8z9mmRaQosRj&$lZd$$r@74ZwZ0!!xKA0@#)t@3xi%UXL zeZKf+V-=sYWRD%H9TWRk@}vfqa7>yb!yZ`JECh%!h0AW`5aAd|f@kRWee z(S`)u8SZQuEPwW;V9_2a#s*NaRcSU>n>(ak!6JP9Y49yA0-`^FhGaGg#uT0;(X{0_ zk{BHIq42~bR`UWYj&^QOdRe2K30;+gRs74FqmeMtC&<8B;%H~i5Vh3%8GTNuR7S*{ zqZFYnEd!*WC`UkIHt#3b9c(u*Gq)ibMasq^A%jT~m!jGMZ{?r)E1SK|rW`FsEE%j; z$K1qhXdIFjrqltw{JXN;_ONFwiKG`~TvjYf9(%1O zAq*sGfl}|!8}E3}nv8D_Alq1t<~Ox5*#cQ*n>^Mg|9*ib-2ybAOZ4Iwt0ZmaN1W-h zk~j4oo7c_`Hp{ssBi-evT7q@wdj`AGZDs!5TFs~WGs%Q6x|X9(jbi*ksZ?xf z`C$8IU+;3V@B~Cm8ytQnnxZyh0AgXGM}IJet3UR;i2}Ub5b`l-4zz;~Vk_6p-*3`* z?uWqkI4Wg$@2mz9nwl85zEP3KP4HCviR_CmRHN4(;dgSfaSORdrr$G9<$7=1nAKfu z#IFxaM!@ro-Roi`PcO;Yi|z%a#d}MLvD7CqAiONpyiJu=Ku-7E9y>s>1tX3)_});S z&t0T9Zcn4^v9_TX#$?dbWTJEWUIGC&GES&p{X_e~4}u_HJyWS)2NMsF_4O^8`34<9 z8tUVzHPg*|y3_F4OV(Q->17B`4Nc zh6H*7QxW-7pWwk^X*>n?t0K)Xm!vUcI|D7fb6S^NYDgx5w+!>9UD!_C$^)?sl6Nne zqTC|K*J4i;u@j&;6NjWfZrRmfjdA#xi2#G-AR9sCOsdi>xxSJ$iR^XUmBuPx*%D9i z4*YIsSxrwE5dIY}0=zCzzdm1GXL6a)`iqd&Zgu;)qgjqe@y3r`kkYFm+(a6pIBw;1 z>iSthgpPv@Z#S96`CI)5_fQ4hB{-N%007RS?W*dSDV(w~Vt`9cRke5&ul}XMvDn9m zhXeSjAXq!ljjPXJiO9naRp8QW2Xn!5q_BkoAbqJ%0z1s8S6H2|B+IG6&Ga%s|Kp2H z>jand9<3hFQgKpB-*8)O;&!*?s*O%MmBXwGr@PS5Cf^yeTbd_ySIL=g@mXW46;Veu zHt$L&?K}Uppp91K`xX-he-bgLZs?wm%eYQa$||)p3|#m6HM@#hS@;-byK+)OJPMm< zRYVp>hTUa&1_>k4Py>48Np<@qw}N>L0}(RREOZNHX@l}1^Q3ax*)zrYfLw?6rC>!V znz#B&IIg$kFNgq`3WZ*n$ix>KbOH*g?MZguuASq#vWl9RZ2P_lxPqML+1$5XH|9hwRliU}M;shQ^&jaw&x>Dz^L zU8;^iVMi9JQ0uPhFqhLC1ss@+O4Z+-ObBkJtXp513H9bUc@9S6YcYW~Os-Qr&y|T{ z&Wk5m_yzgLZBHStsY`l6=TlbEyWLEmpDj8uhK$nIWj?7QG_WI|51PmLhhe+BPpFB} z=rpz_0+c^fkPl4xjhr`r)yOG-=G`C=%nDvE0uIwW6YGWqHlU{7?Xq$m=rc>%bj99bYw5qM!o4ym5!`^@6 z4y=0tKSk!T-UuR*mxC6yDDve;2-1Kn&qT3dAnfYt(g4ak(;f`pCh)1y;$tto$O~2@ zs}p|~Jv)tSY2X=qOiR+o5TVU}s-I^JpTjPQUu<)q+b=R#aiPDI+ z;pjUXNvPm4{+ra-A-Osa3eyfzMj|w(a3K+ySPbYXRsSk`blgLZt{6gap#-CW}MZ3CEH6yd~CW(Qb9xgqi{Px6bdWb>=m7;~1KH zqL{bN#dmQ08qNC1x^EfO6ShoT)N?}oO6{CkWsS6U6{6QLhz%E?z2oL&0r-0*Mb&m_ zzKqe~OOa(CU<)s9W8zV$B~ZjrVDL%hp}=o*U96^AzfwamHB;p%z3B_2O z?n3Wf;AtpNNAlhbfN8^bTlQF^u6I7qObwzdYlrdP>f)|J*{IrSLGz4)f`QD0>_m(hD?Xr|XV;^p%ds^1e z8Re_fyt8oIUr*JfES&o`Ta4mczNGyPAqdnJt5MbF63)`!Nag3e5pm-#op#HjNkJ>4 zRM~GNdo%~g9AGrIF5BJ#Ix$||@oBMst8s6KlX|D39n1?jkFKz%_p#jj1D-J(5>EAO zF47R9g~%!G+PvC*9O{R)*c<12huep&9BADHiw-#BF`$%#!Oy*W_hEMWqg# zCH7P)=Vf0lzwnTBHGdQ0SJ`3HJ9XAyPu0)nmB)wZZ{RCzU?BVMR})xTTVj`M;sYS%5>1Y76S|FJD<^xma|`W{}`ab zj)@zz?li^g%Wk%oJ~%k1hcU2j-;d}^#ib-+6HS_(FL@xQ9zrq!i0sU58y>Q%yfQmB zk|@wM?`iak2>a$*G=di!X-Z#p$Fwfes9&{nUy-h4`KcyKf3@ky>%3 z0l{_J_4BlMjE_JF6F~~5E4eipoDtqoxk*z@^a%g_ajPQGOYLK3W^k_u4k!1`IccHeBx*?X1*?{A3u5sb?;vI99R&#)aHWqo zz^2FI+rrESVPMbq>o*|`p?B@7-}@_2!z_DmG4>%fpD-FVv^0TAi)dgx^3n6IT&+Sd zBT&%xy^@7V0;o3j$V_>_Y|`VJ3jQqPc$Hi8IF7%k>vNdAX5vMnTa4e&Zu~&jqqPXb z^h2wR)s1yW{P>N#S&#od17XZ4<9b!kn~Be8hhcep^iIcLFMeuBs(J&XFL%(w+g{u* z&5L+@gdAaqG}q2@yIB?+8BDJ;gING;fLew$Rj7B6$~pW%5v6faaY(K$2IgG8ESl1x zU$}`C_u=c88#@jXtr@$Vw4CQPr>RF~D|1%+jJ>Mn_2VT$&p6)S_5l8uTKm-Ci1hmVrHz>yXLnZ81bI=o#h;Avc3F zB0PlS`o9%k#4W+zm1MlSlv;RxZ%3V5`OK`9_O_-ZCftg_ zh(#*|5phamv~&o-jIH}_sPUFhFQkW{358|k zn!o6JXRM(%wWF(&-=@bFFuTI7M28cP`btl-H%8?eRMAB>7V}6%BcvB zULZgIb9GHcb=w$qP2CC%9Onkeo9}ZHRMvIM^ZI+$I+B2fZ8tV+^^|^QzSaQJg;lw0 zSdI-MEc8j%zoq7qVwqQ6?A!!VQ4V9H-=sqwK7mkk*|72n=}~|{LR?+efxYrEP!-mu zd-=2M;^fM;@u=@}>$|1rh3-nPTiz|iiMYH=<2T2#k4%y;MHKaA?rB-Y+xf;~vYUh> z%7w?k64D=7U`SBXw9$!6x}Ha0-a9IqI5y+U{=_;KXY6ZuYGJLgEB8gyF%!YEqukGy z-!K#7?6nPa`_2d5k&KXi>0G;lXD!`>f#qI9?hNgtn({j;+#6=Oov&ti22nE}d&z1q zewu>--rIP;96q16?wcD>fJBAo#=rYgPJQ~ik>hJ&P0@3k@c!l6<6%ZN8QT%3K0^~T{_aowp`l* z9>vfLK4ehseoAk|{8j8MF2mnG{ef2rYmJTvdgRrqwZs<}iF}or=h7xE)%vZ#8wSZ0 zMNVc`8JD29_qu>>Yxmm57&_SF_C^Jl>!>jCMdJ_)jxE1{AIorzl(n%;^NZCiv6Df| zlCLyF6h8e`ve1L%w@K}tN=kw`=!41ZgSdE~-EUz&A)#l>pg@5peVnqh{I)YCV~!v{ z7pvy>4XY0}Hr(eoduG3fx@EeCTFRCMz@(dU&L2OgAERoYn>2ja({aVTXq!an(bq*i zatPVnw<18Qj)xPmyLv@MQFX{RJeLG2#JX~Y1y>b-a@V1O@0PC_*(sv2oY0-9vlSWd zhd;{BFS)8>y)pl3&8Ot1z+rzHRjJ~}OU?32M|g)<^{o;*WbdV9YCeTa9U30ck5Y!X zt32diD_ES4g|d3wi}%#bxxOIw9p6DAofl_u({lMtt68!%g-=jv4r!Tm5P~++^*)6y z7_^$D)#YsG`Dib zzwVv*V8>@RBzsyFU1(C8h8vj`4O&)4`w&6B<>SJ4*3wuwPcF=@zgyN+dNryoezin@ zQVowLtV(`~r(h^>u=gNFjsRywN;4h7a}TdWvUHehNlX6$3Pe5O;J{o7NTqm}^!+@& zOf&od7X>z!k^&ya$;dI;N}#6AxRUMn=Hcr{-Bw|aKb@T{RfWRex^Q_ZP92i2%j<`JUhO&Dqg z(RZ?a_pxy(c4lrL-lZ5;N1Zl_3_%)bE`_yn#fuB^4GWnXVc}vzItjGt7jGCX=P3M| zAbf`Ax;RjQ(DNz_hM+*{2k<0TBVwohiFs90eAdm*f!CLVgEoZ%qI77sriXmZYMpo7*P* zke2I5-6ID4u{_Z<+mOzauk@RstB{fQ^SjeDsN=B#If>|01fb|H1r~&AzCi}f4hG2m z&)4g|RuC6^rN;N|tmDSkQnWi|iX7ed`S$1p)2(hFvdem+uuqs3kj56BV&_%5;M>tw zV>%lDqQE@BMQsxu!O%m{AXEOa@GTXHD1cdF^cC|EWpH`BePG@)5(IFRm@wjh(+DIGyn=>qC+H$eNLN^N}Zo8 zo9(bxtjm9fl9-#n*UU+wH%to0*OC#14=UwLCz5Bs150xWb!WGo}HN+_oqWXa$P(vJBc z%2cY`rW_^=6p%;ensY}xDWuUqC+PQNs0O$Y{<RtL~K6hpizg zV?nP|XD5A6RL*OUn>fF5_k*e_t4lxj3;gU%)g(M6bjfK_w9kSFKLqJvIv<5CXdtgl{pl@ z$;I%OzlVkm#_84_lx|C7+SYAXqCyV{nD%rkk+Y6BRqCON*YIq~>? zsPt*fr*6F-mJv5SD*;oU(m2Vz7ZN{I8w=oi;8O?7GlJO!tUK3O`<};TAVA}Qg`|p* z>Pb)IZ`wl`VgN-7cUb|Cx>ZmEyeymaBi=w|hl1!Ae35Qt3(}8Vnbor3*6@lyE5N6*rJ;7xvN-17ky4t=7FGGweWaW>XyzE{o%DSI zhrs&BN4iaLY`-@-6yNYE1Z>olJnfqTveWsGkcQDlpTF{a+}D*Rk&Q}iks1`|_-Vk0 z9Br7eWstV?Zg>E1L1s&h#e^E<;01X(nW*SOqD3q5EbY!&&VtcE?2tI62T_Czv2Q<#;m9ns z-dIZvapua{`-KJT>*p$cKXJ%I6DBHaIK8!F(IV+N3|wVA9p%~iK*RdM%too6c1xA) z5+(HfWkD)2Oa*@JSS zEH7AfJIT{7sId|Y!vi|e;At0D6CRejZMe{~rd%F|qu2&$l4mDRx|GWdkB#*!mH2M+ zlqoHvFr$)!!m(Ik#fr7lAGuB~lar1`ttU%y+RwuF^lW?h3msx-Se1=n^~$Br?YMLw zP4q3;2dMH^lw-E7Zc-JXNR%F6Ex@Ra7tns|b z(#N~tJllx1qPX7jP>$T}A$~t^)@(}zuAnGo7(pKZclEEV5P5WPeg?XbHXaKKF-wCZ zb(O9>-Q)fAGNz`wn2*zp24uBc=?G^`Huf~q^Ik-D=XIEbF9j1K!h#3N5KAqa)KSClVy z{}v~GVcAb!c6*Rh@hm9?1a5JCGWa%zdf@AvppahOH-k>&EQ;di(xH{L*4h(rtyFp) z=#W)*8Co8;jjAF-|9-3F7BnPei_bKWU3GwxG4_(brXdU=n*tb;szWQ?NX4!V3=g?3AI@>$fPEl-uP02OE*2H7(ML!e0DG_ ztn0o7<#w1K_VgMIN*lB7-uSdbdba8|J>-+AavG9MtsZhMUR+;-6!dCMB+=W0l@e*) z&v)5?Bg^Ok4p>d9N^)0!LX0ys)OR3;!IK#m)%AxT5DQd5Q!b~?MmrFZG1*&M`8n@% zR|uW_CC@fwH&M8#CrDDA(m1Grl+am4t4p2lsyhC*b=NO`=CQxdNC&Xx6NQ20OWc1= zXtVbt`F4zB?<@5ljJLe2l#1y$NUFXRdVTrG4r0Q1xuMh;7|`(}fjA=ckq1oIb=kz1 zYysD9)`)2>=ZMVUwe;7;BCNj&3xt1QIMoep zjgfRRTc-IRM3yi0 zW`5M7Ds8r;4+6xs&(=CbtMm0q9dqbB@kMR^Fp+^DNgBj9$>AVpq_J(EkpPL?-f`#D zIzNj_i$9N}t$HPC$|__ftr}kb5>1=F#(5i~w(TSEYB6Iab3DzoYa3>vYluO3UK|M< zqunRXcxr;E4lUJ2gO9VWjHg##AfD}Aep$TL*@gZv#DIm|hz5tdLwcGnCph39uP*hu zDo&+B(g_KMhX~BsNTHv`1rx)&2jCr`P#4hw5vI6faKz3N83TEhZdltw%P%cE(TVJq zw6Q;SflftuV0AWZO2r@dg<9_qQ?6~-xW8|%0fn!g~Z>i+B zUM3Py4qYa0{YkZLgKCxD>ma6Wnxe`J!F6`IxNKM-rK&aN^iz= zuc^sE<=#gDAskVnL<=TiEYGA8sI(#en9HHGBlAjV@=|6Hwu8i9@)UL5J(%ek$22Kz z&>wX5ZB(uf2>&y&hL<{{uUe7sq6_wrTeXT=7Q&8MZCpR?2fDeHeB^Et!LF$*r)L2VpzH4KbH=5mO)Jih zex}YkbEaXK3YxizGH^-hUckcX94>vJj(~bZRZ{s7ITND7I`*LVCvREoLt%ra zRD>W*nd)m5^znN>Z{E{by!5xe=w$V2p`LIWbk*EZj=UHC8TI4o<_%Xh^P!b8OM^8k zVaL%t!E;2JJ0I@_n)^~syc=xgg#eZAm%ADuco^RH@bC4O;fNHf|CJ?(F_ zRY@@T!JMn*wXLK&rqgU805OK5+`UvGwFuENkjn zZTe*H#5yUxUjdpWXxj<1{kp7tKxwk$TDA@qFaXzC<4XQn=cBmBFv6-H<>LZ(hKr6& z+925+{RuXeNW_uij2nlxlg|+N^FFL{a98ZSUTwupWPVC#*iSen=(urr*As3Q*nEn| zkn=bOmoHq|C6iD(BsAjXDl8k2=cbldNmWHsaQ{DtZEdLuv0TZ+~EN^F@X83NcunLptoCx$){kjfi= zNISK4S$}1Dwk&?S$W6FDCN8*6P9mv~KlO>ws8aeB^fBlpT1`5MsRX_@tS#3*v;2M0 z;`#Gg{;r(8vzpGHE+}a*+v|k3S#ij)u4{Sgdz3aettA2F&a^0Pw63P%w6GKsM^s^I zwT&~?b_A1vUwmEK_k~d@rq2cBGLAdO+C)t`dX+EfMJ<&kyL#3Tphdj=fr(Sr5GW2d zD146&R~v&M=0{oWS2hmyosb|hCM+y}g|PVCw*=!uS|I-LR#Uu|(ogeeZ6lKQ^N#xV zi*+v@rA8v(tNPql6(#~bE1b)njO654EO<`Qsx%#zP$p%dF=j>4bv-!b8jL2C-|+6J zt@O2jjc%k_72OKy#39v!bg45B=6DszsXrDGV0&)}Zg8dE;X2IwV*M8BLT|FRC8Y1Y z)Tw^d>`tTGp6OM0p1wWEJKOH*@$-etHoeH zS07O}gj~fd2!MtOi-j}hxVtqSl${weE$XD+7q!QFedgpP#o~(UeWni$kOx$8L#0K1 zP&)H9x!khQ?_@TY?xC)5U4lxH7e*_M*VeU~q^Oe0zTc$!`alrgx&I5AFW*lr7j-b1MMCluhtFO_Hcn;ExaUmyI21+Tp> z6(%jt@8&bgQ!PmX^)ufhO?=sQa}CGhV;}iRjxDWa;3&3<nHE#F|WpD>Hc{X=*)Yzan6*U!yZG80I1C0qdCoL-h7Q&ca z*`*O*rdiC?Fw#Bh$Z;3-&VCEz(6ExEYZ7qV1JnB<09hrzlCwNN2;2bEc7P&;BvmyxPFSoNueuOrNYJ z)ryNBOg~MswXvynu_;p5=QPc(pC(AHsUG`c*LYXgU{ejh9LZG=ACO{ykEe zeD8oEIhDU+d*Cj*81Y?5N6X8+X|c&NMxQ72@O81Ag+^He-jTn(b;}*x|*is^l!*!O(wJ-uzsE}jk0orLOV7q>i`&e|Huk{)zhHib#!5x7kdnQP>*t$2@z^%$k*=LGSh(6|G^w zOC=?@F4i9mr#Z)y@Ae%Aen)E#%x*6keoj(|{HM0QS4OO8Ih|i6G%`*g4INf}`%Dns-Zld@X*xY|yW5&?myzB1)-o8s3zTy8I@ zVf=0auf6rD>zgKq4chMz9Qc5?5iIY13_xU9Uk^6?sY38fp;lDa#;{3cOJ0G=v~Q*6 zGI2uk7fW>BGK|qHrq5|rUE2Xw?Td$*%1qXM?pr0u=NMv^SzDL=7ZZMT9r2&o?u^cc zYgp)(bm8`A3P~taGXYZSA(Xk7Kj}BZ9vSazaec2{!6Q}y)!yMYOv%EaKMVY{Dc9w^UM90)S^7xoVzw$90f*eGe+E5aQ zL-JZlIH15))7Z7_;XS7<@+ng9gGUB~j{x+JF6D2xM2kT`$gPo3`@2EX6W%2$^Vq(O zqKji{r0k^4eb6IovTM@4WIX-fTnP5yYPuMf6rsLr8+cc1kURfmS-Ag+#;1gbN$tJU zS!8cKB*}c$Ao2KAYr5sosFdY*G(V1CtvYkh>gt#Yy-{OqQnMybk=5EKl)jBfNlRznu-XF%|c zxs9FSj5efTf2-~P135v)zN2M0rwW_K1_o?70?LrV*M z^9=9l@*2FXyjz@xV{LdOUU;;zUg7{0Ig?GM1AwCc^u4Z~ix)3&M`3T4w@mNpp4Rmdxbg)IjFkMaLK2`re zRpG;UbW@l&s-^Kh=Nf3Ux3e`Zl$ObZG8o&89qWdv)^nDW^t%%5t_q*&qh)&EP4Ugs zHBN4y>m38w)O=`Bw1*%02_{I&*ytJlo9Gf)eqepA;sy^eo2o6eCrp@7VYpd{i2vpa zAL4sxo%}D5+eE!*)>oN$>8E$|_5V;6r2d-OHA#x%3K^3RG`p*V49=q~qck z$;1FDT^l@aEcdnIqiK>C!hlkF3|s>~j7M|ho+_u7CMz?92N`a-6AnCv(`3Bc_q^lT z+|4g{zQsKojut0(!^_H&#bLY{kNAbm-EhRsc(FKfgUlU%!o;_v)BIbU;k>r*xo6_h z+)WNF4%|skm`5Lu@TI)7#}>weZTknx7~C>*rt9|f)9!w?ws%pb>6^n2bI9(m6Z{J_h4mohIQ z#u5TM(Ykf(gqTTm@7_JpuV25!h!G!d-IkptAuB`f}8mTQ`{}D%$+;$R2VD#;%0ao z4(4CAQ|~X;eKW%`c{l&uf7+%^V*U<0B<4#VenEP4^+XHWpSHd?&NE#U(%=9mR>%>ehpXS$=+F3P37$>ZQ;IPZ$b?-nmk{PgUz6DR)ghl$&7y*1HOhGE>; zv5EQf<|Rgr8s)zyi~$m!=`&|0CQhB2a54lgshoB@{q)3FbLS@BA2%*>lKlO3(4fS- zx_|SV-y{@iBJuj`32m5=_^ZAroNMp8D=`lo|Mf5be-9ZJ+#mn-wIwUVQPz-o0bTj{dHeL}JdIISDZ0d#>Gg-#ww(a|vx(lCb>2{rKaLPiTf+Vw`y7 zfA8MC{eAT4(LRre1NTAVojW{$i-*#{9i8jJCZ!QB#sN-d^3?)x->E7>Z`qf z*Q-~eqrw@h_=uPI36~4L;153G*W6D$@kE~<(uJF)lXR1Qg`0)To&U(` z-FM&Z-M8FwOMgdJ)UB7e;)*N$|8Cv7`8$}X8#i&f^3izNeY@=vpD3Rb-+Ys}e$PEC z+(#XLctYcXiBCWM)Vooj`R20AE=y>&cLJH`{{Sf$?l<3jv*!i*Xqlt<&y;-uVyBjp(X zhA-tD9PhpNUhl4TVgCNY3om&8zma_7zpSjx-#_~3BcIpjoO6!pSS^t0Q zt+)2~2OoUU<9OeF_xb-rRZ%1S=bwMx=QZE)-{hKbwL3uKth3JY?pDE?GG$6))~s2H z!wx&lyYtNt^o$$thEriqRv1n@^Gq-6iSgqTOZVP8vGBO#5--aCqc`1@n5(>d^ttDP zF#7iO_q)W~QsMZ(-~Lt+HsQY@9xuK0Qt#GLMj>nnO!wliHU^|9V<2KOEm_mb`qJt8 zkbpZCIPc%KX^?RHKFiNEV*#@53f|SglN6kruIX_Q{ix6zns+VX^3#XCgMRQQ1yRxW zY3?cm0qC(>T8!sx@j@e{m)g=Vk77h|_-)#O|JkpG;mmFt# zT8k&&nmX0(w$DDUzgB54P@nhXFTCL1{@?>wUxTKvhqg@Vs3z;Dn(o|K-CftIncGQ& z-W@cU+)r0`9e~qae*dH{opLpWzg2VfeQhDVTnm#XD4eTcM$KVpzHoeBJWfjTI7#6` zW3?|2<0;?_Emn6HB%${+;iN45fBLDRH{X2Ik4Z5N+lXn{;3xsCvW9!pk(cRk1XtB+-6zSs9-(fm4;9{ZO8?G^`w->hI5pvi3r7vnyAR|( zMNRcEkK`3RB46-pWIS44S~^KLc@3|S@o4h+G5Z;bEU%@oe7C_T>Slv1JQm85Zc8hD z^^g>*>H_VwAb*Dc-%H&XuWEK$8_DlHrR8ExXYZ^xNZ_}ULvXaX7<+&S6FFt-J7jVy z1?h1_{vi8~GDI0bUZF6Ld`lW23+J78-tl1@kGvyprr%5P`0|4r1lF&;_F6?A)7KAC z4#AN-WH_egwebRvs?EDp96wOmpe!4YCQEN?e1N<%3>dE<4$8YKx>dFxlJW37l0Ndm z#&!4}9FYac!ls<3lgIIzmadk_dOV_&;L-Ghr4<}y%qn<92Ehj&k!$!enyqK#lyJrg z>&}u3_?)SI(EcVJF-`LF-it4~llIudjnEYN&ML=E74LzPfmhV6J44gp$D;#OR^CK! zCiB{9Q^yY;f7~sV9y#gMQ{5nSo1bvviM~5)W>QBO54F9c>#$Jk5kf;h9({ajiTK2; zN#^@F$#mRN;1J5hjKO*VRpVgal1V1~e9}vb5?i3NqV6Pm__TAhz$2 zU>~9Jh#h2z`sl^pQrh4C_P1`%gAcl4%IrlF;CVm%q3gWE4z8h;4*i*pC1~Xm1~R0% z#xYilzZFW)d@1Eg@{f9-C|v%6aZ;YBB+DhBVdAk|g{7g4j%v2%?8@X{Eg;ug?q(o( z3Gd)dnyf(LuG*xZ96+91A!9)(!VfkSI4g8)iitZ|hH+Gr+51I-h4E62)_`W6%nNv7j0j7$R%o%AMRNN(sn`e`IV3&EdMb`6#Z);pc}#~RX-dVNGKM=mT3!<`I5J30+*TpL4@Qvk z8ukm0U`$%WIFf!0kh*uO;7D0Q+2WomkCY|c)1{Ss3L7`VC!gWdDhZ^=Cbm#vv;ls# zK1h}~GO4l-j^G=1CmhN~Ivn9WHLo+_h)#kBbfXy#@S}V{7-G|#omIxX4rtui%}iD_ zyeG+kYPU`U8g38if$zz2JLN-vm4ViJW~`%JhG?0LV!ZF6?WRiexRE2> zi#o!0)Xg`$^G-R%JuhR$7%L|+LKe4LARb3#0sOeY7T&=ZqlPS`qk4ps?nSW-2yqh^ z3=SPSOh&7oFOUc+LQ6sr7KESKT(lGrCK87*Qu81qFgAn(C=?`97zhZNMp$Gh?-5+? z0E6(G;Xq-iAJZ!j-cw-s9^t|rUep}+?g$teU0?CH6X0FNaDSxlDpGQ<*kd3iCx7 zPcJ;;T^7|T(NsklO>~2VSAE6dBq+?*nI`{@<-QtTB%tu_d6a@R97#U~6dW~gv$sijENM0ddh3bK~MRI2-^xV zih{hRP>|Qi2+tS`@-@sOg$Os}(ONk8Kk~>Uy%MG7%s1c#j~E}~Kv|&FFv8%2f+5{s zNv6BX|L5}i{&w5BZpyzd%Fm9HUv@2CE_^v94;+aHypUb^$5?|Sd?CN!f`TJYs1Tvp zkW<5vFi01Ckavco@rZ)OIN(lP2}f{gvZLbjF-&AKe7sumr!jC2;URRdY3Y!D1+$4EdllM_v~4= z9v|s=i`vclOLK{*e+awZX`p%Dz=4Ss>L-0iV*poaplfh)0PCVFuS|TXzS?)xC;R+s zuT`cqG+3P)U;WOV6P?sAIzVv@NnX8$mCQ=z_sQpaY1~r7tLc9gu8KUWX&8n0u8Ag4 zfQ5zimT*pJ0Qx%dhuaWgrQ%ib-uxBfuEJjt?xBYsTG8(>1ytc)T|8Zqyqzw7*Y34f z;!5?+^UZ)z*wuv*e$U`__p2~Na7t)V>m>V@Y2)?+C(&303e6&y)v?4E6 zE%kG?8n05zV31lSQ-v#QX)OP;@kl&jImIvWXXD*?vwJ2Ud7m0iHeTQZ`7jJDpHjmh z+;nl~>z;`h!Z8`;+43Wk|7`9x<()8TJs=mfo@fmbmc?cF)bO*pd&wmXrHN-oZGw>- zt@{Vep-OYtMGaA4FziJccU6K2gCxUcz!mS(#fYzE76nxL1M$ zv1K_6@)(kzspdUR)weXR!bKAr0sB-L&6&wQ&Hk&ZKMbt04G28c(-kKEd6yJ-^JD3- z|8~#E9l!7f*>~LS*}}AYl4g}nO5RR8uqoqIYr{AY5vclMq;S0Q+@u(^lH;tIR*j5~_h-lyKv`A>DTaBAyL z+~Lc@GoI|f-E(m_`9+qDOt;8tnNxs)bP8R8bdTQo*T4FA=$`O6T*U_kc&My86(F!& zf-sAV-|%xT-K(U@t><-{im9Zw7vSCr)X>h znk?f!TzLVeAY61ohY1n#!q)5<4_X^1sxUvM{C`G$_=Chd+s)Aa45WZBa;FWg{N$dp z{C#d}SftYxvRr;)GGjN41u@x!aTbh6RQMFFTrnubKou6(2Bvt9Dq;DHVD>Kn3R|y! zb~iq0Jg(Ou-HTELBQ#*fitiz6b$qOM3zRODf+;VI1Wil^Tp4iVnRM4gX=LYe(|8*; z2;*faww5BkRfCzkXmSE)l`PW4gdb=ik_{P$=zm9HO&3ySM`3YrLT2db(tJ5j|5o?q zx9XlfLNoHZNLJW+n3efK8!zc8zQqXC0s+%0biv!KDz9rK|4~p;3<$F{n8Cn|1#3<* zWslhnnVN7>B(|nuVhAuE1Eu&sm4P@}{fyUZ=EL)9F7BgQ68+@Pmg%3W)d8URPgM$L zETha!MZ3vR*ob^2Ma*%b@6r;~Jp8Y8e^h&N-J)(Sj0lHRu?sO>j}ZA2HDRnUS_8xv z+tQEGwCq0W?mRu&#m4q9P^z4G$M`2kpa2NiSWuKYHgW?(e;m{FG;_u6w%cyLId#Ml zN4Ra&)nN0!qfB4{^1&DKUMlc(nXohL?NUr^M!2uKPWsAtFknk}6<|hV=L?aFjV}za zMh%1er!o{hl{e6EE&Lm!mfp{lPjAVPust?P93#SMaggOxp^b@!hq2m9T~-_xb(Ja` zqckP>7EK#{RxQ?Ha-S|)GQXao_$NkS14JOwL3Gw`(2Qc<`~jMW%=~9toM7_w?^sWK|c*D+KY(v7@uqJ@tadJUS>X+a|ZXXsPrum>V09(&rwEoAHEi zl^>k3!QlE$YK3g3ZA3m%<>c2gBx5xOafm84j6Hmzi}4X=%TbGBoXU&|QkzY|Gt-DE zn@0GxE-TJt*;exGjfckk?okWqO?B5EtgfrRiqB8`t|oj7H@?RR)O7?RH6R!csI08a zZ)-rIV-@5cd+gy?O54=3OjG|#2_8xTg~Ip-C;DBi8CQF2me2K?yFXX|x0K@fLP`e! zP4SeH-$KW2#y>UOOzx@vO7Wk3AU~EVUl?3x_7k*=rcVD{EB`s)eT>ef{+}u_hiQ=g z6SV{;${(jH(kh{%(AjOb;C^WniZR z2C5}G4vnYiV#pk_#cg+dhMzNk8O@tq;wC?65zs}&nd7~bU$>|=!gaG0`_Wp5^N0+| zM2%xTs;;Hcn)0|kf3zepMkxAqdS0in8fpsmOoj3HzyIC5`t4NKYIfZWxiHQG z{a0b{scl!DP}qlRgF6;CvUvrYPZUCC!efTukqaj96(t!Dc8@ZyOko=?rQjh`oSED+ zrC~#JkJNy$#|+(5>s zD-XG@)xh=-w71kHTE}yaoU=c{)HF1qco;BUR_eB zX@k1y$x0GF?^IZvF36!lSE-wGG?Oe8FUR+CJSqGXs^wv_Vfhl`v?!0ni(H2JAmUu| zCkh7PgVpGz#DjFzB|P9IYKqi=z&u@6jDL{12ppGnZ84Azkx;s^@I%)ChNl#QXa#~U zip|B(c=3LUjNG2;zPee;Ma=n=0KZZi*x$2I%D{t=<_A8&0=`N?#7myQ2i=ccrN|f3 zPQ36z7g9}#O|+Q|!43k8l(&~k_AXUx@(AVWacTvRkc^HfD|0{9#rtoRR?dz8S`$C! z$Ouf**5gBT-uuaCp6Nc&RN*}}F#qBcPq=puImCT$uf5y=`Q<$NH`SW{9)tDj#@b2- zilc$ay+X?icu=muqZD~UoX7*~4Z?C-3SLZ3!NYb8u8EA)=9^SJ;Gp31Izc=gBcc4C+%8df$AQWS zy0)|_UgX9@QTW1%0VyRRdu0vd!yOH|lOMTDiAFI%#YCDn-)VND6A1Er; zdQnM4UP>h&h6i2ZrQ(6~hw%u6sQACehi&)?2e?H+b;5vH;#RI)JPn#25;!cXQSDSE?3Kta^SBJGIhsS?QswL4Klc zh(BrNa0k__j+dfPHQX&zO-p9n?`sr|EjJV@@6_z5VMZwcm9bTQq84Z4Qt&|wgq9*L zl2Y*lAK^(CRXMKtKk}Y7hi#Qjp}>uGCA1=#=G#Pau|{b>>G-mqf!b&F&Q6G#gEZ|f)3THd0k)WCjDh)Wo`>y+sPki z$$z8W6gbOc9bI3cZ~dPLf55`I(k(zpje5{GRGJwfe=##0(7@RvGJ&YB!1Wg5s3Ao zGM$zMC)}|G#toXk-9|>^#^k^$1F3QAV-qYHagt*MVg#y1V3Ri>27vW@AE9ZxjCVYt z1!SE6F(>Yxs8)z#4>1BU0-GiRravQ3J$=a5k8JnIBdsO^S{4|N?&58R#yKv~276m* zR>|Em9uNltt@2N$`H8znN`re{j}^JFtFH4}EdJ_JS*Z)W6pM$V&BC$-VLa?Pop70P zv3oi{g>u7yh#A*fJV7Vu?1pJtFnAj?uVhFj=$Y9JrJ^tf;pbxULq1u&srO>>Ks;f2 zF+PeJf3~>brxZL{ymrsVV=>+t26it-oZ&daIFyP9)Nw^_K$xvUV&>21?7o0Fi4bBD5?h3*40xip-&);BxD{g_vmZQH zoYQ60ex~gz7N{HR3K_XUv_w=mTZ(Vy_?JouSdhea_oaxR8Vm)I{DTkj1!Yl;dM#7j2iuvHg1}*wHb~?G1E%jn;DUS?o8ZCl0GndV2yr40l;NCvfDWnB@dAr5Zi>0L|lIP<-P@S#~pXLE3UZ0&6qL6-E`AUUO}$V zHV_m-GM?3>d`F_ABy5&8EO8$R>`sbvI|<(7S~w^MTuV*kZK-=b6_BX;$d*Qg5j;vo z2qJl6d|*&YjR(rA6hyj_OR(S>g~c<4v=lX7c(8maRi2P;Tk(vXA}^)jg?PyiGiIgW z0htWrft;2S4`dZQqNYd<2whdQJkC4sJbw=Q2OoUk&OiTrpV|F%PV|2J?dSgUpZ_@9 zOJ~iRa_vd?P0?yo7!c@s9X(2B|3XF$V)>vg6!Q)x`hh2Ft(AxcA1yV_7>~*v01qlX zv_@vL)lG>Ag;f{igRbeiARpvQj7Oy~c933!&?8Y%qy~gq0mK&Ev$P>0`zZ|_I@CS< z@WXEV?YDOwIiyEKEf=y{m30$DVZnit*x<~sC6>_bqfHAx5^rpo*I!M)rMepHDo?hG zg9rh6m*Xd9ZxpyJF`Jal#K+a79g##(=?q zUVZgdchErxxo+LM`RAjLKH9fT#*7)`-!rfdVJHzw0N@_D72bN%W8G0*0ADMPOVmng zFQ6}H@LaT3Zfl5NSC|j~vdBM%1$=N7n=Ib&-SVZ_ybzz^!E>?k$P^zu6&n-d#o{iN zl7bJ*6T6p+7vhB<;|Uol1zDbOhf0Nq!oe9DwEv5E?JFVtMnY)s2-5_U=VDVSgs<3X%qPAW zj{q%juqz+@3h9^lQPy=OPwEmMmN(#2Dk>Hpks1)d!-%ji{Tpw*;j!$hGoxR7?KS_* z3?n-LrdVKOJlae6C#i2rfM2zpy`R#+3ca~5_h&t3czBM>7HBM>8y4S`_rA)Aj{ zzQYo7(jWusv^Kt0R}lx{&{t^2qn1(R{JG8R>MEX~^EJq{T!!R)?X1cXq*L{5esT^p z_821&BM>97i6RiW0kM_^Cv?#o!Jx2!kb!c@-dEzENrmU*j^FkDV;(rd6hi^+Duech zc!IjuZ{Sww9t`_?rz}6~haGq>gnJ>vfSV}8A?$SFr*kVLFNBqj(#0D!kLmcpEgdhJ z+@t0(A71k15&!9^5OET2KH+5Zo9IAy1v0k^s)6YRJBxnlkB?NLNi(ZG%r}x zI4hA{0blV!5LY6R8ji)8iI+_76#*w7ESyyLZ0XDtcA?x;dEtM${K$-N<~*t`j}~X9 za8h|U|LNRQ!@y5IxZ^)v7`SB%hws8v2p$P9Tb!wU7LvzsTI{)yykz3t;<7t~=fs;z z`QTd}E}3{qb!QTgVGvgK*?7ssdurJE@|cQ8;r~ZI29g`g0L_40Q%#rbtvly{qu6N= zFm<%E+*)eJ$9!cD@63G*|INB}tQ}$WA_%8`{ho4fplPQZQfk{FHgc^RY~hysf59vdqC-Q|zjE${*%T+QU(Meb~y z@s<7uVFx^Rl>2hIr}EfFZcFqo%wwk{j|=3!K;Z-l7G^~AU5u8>&5e<+f1om{e~( zc2S%y^W~B9 zz9<=vLX!+2zZ(73WqFNzSrSLez-nDkWAO-%$my4Xljps;+`%yukIUqr)oO-sdxcMV zU!-r)ns^}np3r>egjSd)v|U3&i&7HZyLV4$0bpXph!KgABS%(zBLS;; zP8U{ow|WW5r@n8SC@ZVvLjvc+w7Z2-zkY|Ld+RE3s(|@$q~KEAo5{VAcWa+a=Z~5< zN~{_*IB`XrPTsv!Mf#g2)F?@G?%b|*rJNN zjCzvCs`yHwCF7{dOQS~iziDFBsGSl$dj`BTY0|lZCyS3e`4jM%iVbqQet97+$U!)a zdiCljc?{BT@9Ndt%*$z)E~#>w9Y>Q93&&(){dij_-XL!C%RL>BjT$vgbn4V;JvlYG z&0QWlCUFePy~Rs@1UMppVf_%`8+Pm5nLGx^O4$wghIjI0eOc$dly6Gd?UTF&JXsuu z*ZMfN_u)GiHy(_qN*)8A8#QX4 zpxg%OuNqh2Rw;w%a&$Vn9UYIZN9Uvau>sfu3qSRa`7I$)%c14bRrduNSijs2AO51- zVv8$eP3LPtPA`A&+4Fa9*svE|r%vbS|6IBE)P0HT)8}@%|3`!JN6PYkCo9}sVXbik z20Z8n4}M&O_It^Fg{xn$+%;^vo$EVT>rVPV>`rdq+bvjI?yg?=t?SVG0M~WsGp=91 zKZ}Qzviw^p>}iVYG%b33&h_ebi~cW2#<{?C?Rt^tv3vK?NnZN;d(WQNdiQ|Gg<8bZ zP40_4k3)w(tC?uW>;FQ9-$6XCbN&0@?}iL{(zR%@o8ny|w{mSgFi3nq>IM#cSkD8L z9@UwY&ep9DmfxrJ&Am#`a)m+7eX(oT?nI^k`4Er2vS7 zpKj70@VLl-4^QBP+<{wp`CR!g(|0S~;>Aa45$sFi}Cmm_%Oe^5^6`f>}7l;f~GQqJMATenN)K1crh`1ief{lVkj zp~H{$e~~bt#qeE{$EOv~D7i0Iyv_7Y50&?agxTXt%P`L)dDN`g_CBxC1yZ)UXz47H zt~lDKvv1$K^nZ!I>7x5W<>ya)oMmM<=>I&03yyQ7ivpe0t=nb#PgyF{eSs@0`@Q@> zukX(E{u?&zp}3ce$Gas*&uP-oq2hU|;%%U-v2c7ydHtC74ckTj=oMMzpK;?Ie(=jT zd`n)#BmYSwJP{Xh5_gcN@G@WcUFLCwS9osNu)F7_Wy@bEfBGv7^{%Kz7X>wpBG7!p zq}dNOXwXZAd|Rzp51Pg0}}nmMdQ4`^76oc^X5b3f3UuL+Pkk^i=osS zkCwv)B8Swr^Z(bXm!)}PbGM?!R&Hx`6>M9-rF(Mz7yey0bqh3UwT(+OobLaZmp9Qi z6>O5%-N%_oVB}PElDsr&(pzzECqF@##+o%j#x!j@!1KOv;aKl~&6;2%Gk9#-a-jHm zIq6Q9P{3p3#%!U|R+r4Et~F~~%J0?^f>nBM;guGC>AGmuYJ`+ka7O!@H9;7S8g=tL zCg%jme}GHl#_+hUc%(TC?}2->X8pbYB}+c5;@-UZVENxtJig%F*RE~g?=4ykmjZ6@ zmMm#6p1hOzYAqhO(w}-2nDC}vz4o5Rl`Cf@)6&F$fTDw!M53|(Ux{OxcVD@ZZYp>* z9Q%9s#fv9Y@ehy9o0G4vdG|GIg790k7%HPi7o6IcBKTalPWzd89@)W{?lAvKemcl~ zgyN#xL7h&?bT(|*HOXUutKrq8NiUDr>eY1h5N?3u(xu%M=eAPzpKA+`N_?9)ALRYd zo%@RSZ~4@s#W3O6#ph!X4td=|ylg2qRpu}+ZN2ie($HtAzJTB$s!NOvbfu>FS%Kx^STr=JK zx3bi%*+B1q{`@zS?t$zCJoXS~R^Hc%ucn^Ih7DV2wr8+{3qIGaYa{oq#ls@`LCw*P ziH-?)Y|tRs$jIafW7oR%cFGGC{{L_9On~hw$~@lhy}TqOL__k(LJ|@}0s&+ZWDP=K zFiJ#)aREmhuoxMotQw_o!O|L_L@Bg%O3PBJES(yhiBejY0+x)3BGll31j0x*wvd%z z0w$1+@W_0>?)$yl@8q1jym!yJ%lo?O+&;I@Io)6XyZih1-eOdiF=LJ_$r!qrRQ7S> z7I^oUUw+iTPkLY|@8ibJ7a6JGkjLoe>z2H7qJ+)X=i(&~fbDS?!nC4BxTi%=E_Q;>v(kJ; z`bb9Jr&WQB8@(dHpU`hi?E@La7)$nEl9`-6!ubuCmb=;2?|Qb+E{|K9W)0({M!gabCv3IKC$CqkQd}F#Gxr6B3~?5bj=i zKPao--a|xYty<-zc0F5U+ad3l@eTQrxKi_zEphg3_A-V%5XNBW2gkpV2T&Aa&utNN)B|j3UaUy>xOF>4>r^xt13Xc8azD;C6 z7RYmg_<@WsiaRUtQJ5&?gC$+)myh!)@!TWtkI&4HT9!h&K3v|Xb(V%B{)~|@HzjbS zy!Q)Uv`ERPM~ME|DHySXHsnTLL-tU)NaEPri;ScZ8)C*~Nz3&Fk$s(niy!0<@JhpR zmdL?4BxT_R!Nd9TLVhHVVQ9gIw1x~Pc{h4p*m*q27>JwvG8o4&76LAe<&aOyyc_Wek#M;Yd0M;^`M0)^4)%|S@4@*@&IKnPHXZl?qwY1j~L1#aUGtFd-n7Q9tTNn zKA9i0a7@dm^~!jlPJ+04dr9j9$-_G~=%=L!gv1^->J=f7a5hLej$}v_A;41D)sWZ} z9Nf9ogv=|GWqlR}O!3tHL*F>}^xjw9f5{-ne5tWKB{irXnAj^$fTqRK?Q1jCIA4FA zfjYvA!=%umOp~&ZA(1ob9%sndA1z?9n|$swJB1_25o%M^WVC=L-%pdJUKzm_IS0%B z#v40JSd8^bLeFebq8*Hjd|txwKzA)nvFsG=G}#Ztk?y@PSW63$aAJQ*e@yJge^nYu z_j+(t8O8T*y33SwktZnSQKNbz_jDY~7n<8ZmE#YYG;?3M#wn0yps-)dQT6y=W7Zj5XMsIS7S>P?PDtg93mRV z0#N304MRalbK_aMJFYV%Ay*1{TrLN^%#(4Pn@i_6>@NAwuDfaoGZ}93zrLFlX106z z%i8V+%aSbzzLn!J*ze)HD?A#$+JnOx&cFI(Y<&2!>{+tJ?+m_2o&#m7-=D$0R{9sh z({K+AJNa#vELrZ!@6-RMmD|S4Qk#rPEUL?q9;e`%6=r4k7!M6!?Mk5y?OA16%6JUT z_jT67o`SyqimzXb|zb-;Pb2+ zMLb``IfWDE2 zAtmONtd`ct;WD_!uDf@aW)ZzbMqyvH)<+}ZHopu3Ltum=kSl@6UMe2etFF4r&lTs8 zcgTdX53a1mDZ@z%?Up(+OxJPWn>Nku6Tri{=oPn>a-TKxO2fl8BdZNE z@XvA|shBl0zYw-wLn$kbOf$s+yGU8$aO3+TWyr!2d76nwmRyZ=&y_$Bl*L!9`=INJ zE-Ti7Fe3}%VDS&Lv0{+*U>Ke9TTf(B{Tx?I>*IXc?B`qw_lMiIc`pu^-)E#Nixx`S zENabggv-7DkMF%l1^#H*DocSu4+NEzLH`fDP~c6o!%mB;=9jc*dAcbUYUdeNCBHR% zEwB79(r9=p2Vtu$MarRJsT{Ff`m6}M=x}o<5J2N7TRqN}2r{@1vFKFZ7+5)n!2i`1 z*zvPU_p~fnGs-E0tUAAS`gFHV>`%zdtI6Ufo0klY8>8+;$e>|py6FEr?Lr7dBcPc)NgN6?t&%BI4)v`RO#uD2l1)<42{u)(6{7U6 zl>z!wWMU2liSYmj z2)0>x5H5H$3k&iCt!MHC6F{g0jUPYW%hZe-Z?o~x`~Vj0&W#~$nw39DA9^{Ol}Cv; zc>^6rk4rNR=|H-WKN&tiw!F$sp#+izsz#1h$=0oGXU$0*1ga?r!2PV((TY^Be@DWiqQY7LUd3{#RK0yl9E2YGrr2vOgxZB<*0;N zQO`6@AQSRR(T4DwiHGJ7EhpfBL8fLI!q%&38IpK3BTvL}CVb7Kv3sNMTZ%x490?KI z{!hk_wmyz3k;lc-t?&WabBlrV$0b8A5F%M6nTj2E$=1Q=49~=3)XSy{qtLvZSypJP5_P*&QOKr~~{5~5&d-9N~lTN9q#<6j|C%#zCWO$_BSgSJxUhisuT2r+;QEVvkNK6n z`yT1K`h~PS{$9pC<_dXm#s_=)8hLn#?5Cp<2s7kH!YrR!lhLpkj8sv0 zpOf_-^GgHfyMAIW5`V=(?V>Ts3Ku-G zp5-TFd94y*7a#tC=>ZN}e)5ZVy3fR4(;?tN)pMwtAU%v~u9^n1t@qWw&EvV^k%!;0 zpScnU01^>e9!r)iaZf+}v|F=gjl1Zgi`?CJ-|fj`s*uMP*>3$SU-^nV^X#+TqmMo2 zKKGf=$j*mC2lB1tN_VxiUasovb8nNqU7nIIuJfLK);%aKj^iXItHf`es;hP!!FuhEOe!Dv+>YYYb;|C_i?z??({z`>7r_T zaXbm1G$HI}gs-v?|2UsEOP0ZSG_TAm9-2QCr=o3@Bkyo?B@j0AVi(@`zyJN7G-!Ej z+_=%5dg`fe;lhRP`s=Uv7H8lv5&d)PCU^qke0|) zA&@Pi$ZwVJuYXyNgp!jEnQgUJ3J<5L52xi315DZm_I_EymIq!4sbKdW~$*-sr&%kk~ z^oGGc`Sz1_K&y@sAmBsZzA5m2Csv|U5pNuboF5Mm(u#k9D~c7rB z&%sAsPVW;e{2<29?1aN)xBKdviSokqJARZ0y5;8?`Hw-qW}DR3(v%mihdV1eZbM>I zp3l^5%BZrt>dFd@_4Q)lY}aKwRa|UNHj*R#N@C@83@uwfrQ0kczc6+K{eEe&J>}8+ zb7`)a@+^W&L#70i^Sg@8@|i-8A2Asdz}Pm&K+rq1~_V$=lvRe-^ByNtHau zGJLjH_T*|11kKxSO1rYaO9t908Yb3a?Y;b++)((2=XnH~+ixTC+vK zpkIF9$BpbUm*efM{kupU9cgzcyKc8o_~9C->TBDctO5!L{lu(V5Qc&34Gi9fx(;g2Y)hg z)>wrl8f+sllsrixH~D*E=x5JI^Mb8Mbp)!8kRp$|Z^YMtE@6WMI|REAxPO&^I*V8u z{J2V0;f7;2Pfsj2ke)u8+EcMj$^Mh)_-$zQT%EZEo8vVc?$Io#r`x&Jg*unJM4pdx zXP<`e;T9tRac_!z?-hL;q!xmgL7{7|Ak%BJZd7r-X)sXvcAQYfnung|rm#berA_cB z%l7~=gRkm-8`XqLWU6?FNxFWv^VA6;5Xt1!K+^cy+4c3unF8l>YbyxFbHs)P(E1wb zA&W+I)kt@$tMh5d@Jl&o`M*(oG{B>3jKAh~(#U=llV(KM15lLoh0=nMt<$qCA#E*OM-Eswudo zN#zb}3}5-0N53X>Z?gHV&v)|TCA|}R6l=$mJCnPG{{W-n+YEuikNun%u9wv-K6($M zNz{>S5s)$~cnkQYlOuRO#b)Vog$`=;Z0(_YlfD+znO-xWIU~cMdMd{_(&cXdnm2G( z2YI);ti^&7KXI`3C{hHCHDM7f@?_PPkc~yiw3k{VM@9+t@oJJ8Bc~>8XOed^wiElZ zAMMqD<}~AR)S$vCoR6oQd{#n}z395asybX)%g&u#Ul%gnew!eJC$%AyZ)zr{(p}S#|*=_A^c+qLB z?BPI$@W#M(q2gB)f~=o&-NgI>T7j1I-xKZ?{vwv;+u+aM#wV2&?5~N23u0pjF14@O zT)s~BiqA@1-H?_eZ@y?<28&@&_ZotQlzrU~2h?xX#O>Zt%x=|B=67Vue zVr6oxhU*QN(Rp0qx0G(lqL!OgTZxfwxRa!Bid}tL?;7Fw5KkuX{`Y7RjfDTBJ$Vg8 zvE1F(6Xf&>#&S8;!D=H^O!1kGiG4>6tX79fx0(|L-@#eU_`xSz?_cvuj4Z5Gnw3up zfRn+D6|1CM%V`>&t=abiP=(U%qN1Gv^)Sj!&q8ybX9*VbR;@4XW{H$Zg7NR~JKGJX zjr&zLC4hu*BtxrM`di8oUnY=(tkR+Z0>T!vS~DFwRROo!61QkKa$_;W`gL3KN%_3q zE49*k>`S=p^cZo@hmQVmk<|TcXBegMTn9~>F8t3;*y?-pge8-oWut8@?h4Qq=gnAUhQNRKulcS)_VOBaZnBz_0V^iMpaV<<%ntM$i zoqc?x<~^BHxXj+dt+{o4p~#K?QdY@j8=n=%BYwNvvbWd;uEzzb^9&BV6sHVu(;d3| z6X`qiK=<9{;-W6lABArA3>d_r##u_(7hxIPUGtjumN2@W^r>0vOVlTcD%uCJRZ>v ztMKB_3_K{%g) z`?!B<%u7R+c>>_XC~gEfh(drme^u=H;Z<(pn^aHbcQu(qgzWHV`PLy*V$(Mr`5XQ0 zjOkRDYMKCwGThge9kH)213Yrh7M4%NjGmS(60vodhG`z5CYU{Q7Xv%Mef}T z`xC9eKHLcBPdjamD-}I>Kwhrb?_FBU-2#T{>VuJck9K6sC@v4P2-^Rg!{;UMhI*(o z`YvMp&-pIUrBF`sh~4H3F9yR9#3zu3|-^Q&+1X2?ArW zVl?c8;%7uxYD?9WzwPen3@@dyGmv1)&&g!4*dF6#J@@Pb2$Vb;hz_!T5@bbdw6DJg z1nLpf>eFgAiED*l{{=kv{4+CyeFPAa0to}qfV`LbPkmKjDPvh@?0S8n3v^fN3|}yk zicx>ja#qVq7=a{^?Qk6QCti3YPK_8cZv>;(4LcTK%f$cTb9w^N((vF;+W*Z8aYC)X zv4=j@4Bh_CITC~qn4#7O=0L)(UDO7^H|GM~pzC6*-kyWFa#XfWUFHU5VSkAceEBVK zv!|tF73uc!6R5tk-<)t!_RbDJoJr*2U?cC&k8%V#FOa0Qag4MAjZG|sDkC}Y0{x8|xybZE5REhf&XaJYUB8@5xJtm&! zRkYQts7iPBzMxlf!)=phTgK9Ok{9nYMe}LDI4b>l>u@Yyu^L?WpbZo`I|4|3$^KPV zY|Z$uc_nx``aT?Xv^Pk>0H?b5C?G%|f=X?7Y2VM%3I%8<~QeWqO=G3YTtquG!G`Zj-dx$2TL^i&C zgS6bO%aE)LKYD+|cak?*X_iuPRdO#SIjp0~lT3$+mNLxaf3cx~N751W*(qh}^hEK~ zk*kd>)Ug@SmUPRJndF@WVbyNuRsEVsLvjkCjCVm91GnOpb>OuoA(>AM7rpo8r}E!c zlG77969DbowoLYQR*OvLDCN}A4$Kbbq>+4)iYav0H=V2vj72Rje z8iFFUgrfyqgev8?T=lM)k;`aL4bIhgrxkTt#JKInn!lp;9>WV)cO|k0kra4ZxDZ@w5{!W*4sa9-{M5B z`FuflzZOxJQUkH77i?kuzfW0)T!!rC>!%&xY(kJntTZf@lG{~gh1XgIbr}R$RFyHDXI)g})N-B^`H`ekF@Ar0{aT_=F;e+3LY(s)9`=1$n?Cv*uey4+b&|?&I7zCyuO3KfSUenFv9$Jo%?pJNMo1yz z{T~GjuA%|!bmT$6FRcE*N`|&0L!dSi3N2413=b=u>~Jb`K}H=8q9%=Vt|fw9ws$ISJk2BdksdRCyfrdUG4RB9mX&}(coLSmryQo?Ozsg1e?b@FCDmznf9W% z*Z7tGU>QloK+UH6_Y-gQ_tJlrw?~;S>#_q!SoZzQa#p39Oyn$VZ9j6+>%JQ-(;{~~ z2b}&-rYDBMG>7^s~V%=o|Hpp5dY2WT5;3<_vki7@Y{5C%B*Td|7CO4x#BmKNy@uc zfo+$VHaD&CnGB^pkA6&% z8xtd!5Qz|*(6s@L&kwgbJb9LmcIrif@m~muCHOt9TMN<5ul8X|fKJ#qB6^(N$Ywvi zkp=`yN>7;PK%0 z{F56}2*~;j=7Dl)u^9i+c#8K@OdDXv6E7TKVS3qPLFL5M%%YvwWQ82V zFX`-MeY2LB6d`N%{kKG!K`*t5I1<&*mSYKVO8w(_udMO}q%3v+R&mWRCbg6&nvcX1 zWabm4^>#)J2I-ifW?XXJv#`ki<8>aeqqt2)L8WsJXKJ3o*@fJcZ!|i-b>g;su>p=q#3Dsj=U*a7RP9l8s5D-w1mIe=5vqLEd{F`l2yvY9f13L|*bi zUo+)fI>YU%FG?^fe#Bd?T83Q5)YzNGV$>>~yh_@z3xs#k*phCuV=!OfQ5^nb94U zc~x1bsBLR2sgD;1C$XhpAztz3X3wXgi>Hm7SVjTbJQp~pU6V*T(JU*mEI+zT0sced z487hFF~nnh2h^s?Pql*}O4HDwhk%0xUHQnKu5a`}v{Ga)uvK&?EL(wPdfVnl^qB$A zGUKnqmWgHE>pz4kbJ1KS*kn)sE0gxsdX4DmqL0f3XT7rj8n4W(Ai;?1A;}c_fQB;%msiQrrwYA+V@`U T3khr|0xo^+n_A@>51#xV1`ip0aCdi?;1(cw@ZiB+g9ZrB;O?$(a?U;X-u2)A z>rbz}ckk}1mg?%=)m7CKsj4iCjzWS01qFrvUQS9K3JN*_62C@5fZYAg3lbp@IE!~m z@1US+<58c0@Q`;(6FGGyC@3#lD5!uCD5wWWQotS*lq)+F)WIhxD8Y0nC_;zqA8Nvo zj1V)O_vT7UP>hf`5)?c%4iqdT1`P>N&?HcQN`u6piQ3wQz(Nr zCW3;3;Rb2yxacS;3Yys4vI0%*jm=m+Y#sh!K?!>ZLZY^2Ec0#5ryMCWXA>uogA2&s zj`B~rKx2DX7m+t_{#5ku-{0*t^8o#)COhYUbqmr#wm&&+9IWhY|2H%jkoo@u?N83% zXn*zV@9KpA6eg$&@-VZ}l>*tC**Qa+Cd$dj#VPz(J^w4~KP~+)RGt4qW#{JqZ`A+F z`ae-6?QQIxG#r2?W}+PbLiw+x|APM0azPa*kQs!ie@OQS{C}nW3tpJ*4_*I@&VLW$ zU#XB$6h#qc`!`33q8K0QeTRY)hk7sdPSXSWs2wr8NIK>9*=2%#Cdv7eIgytWrL0;I z9Cmj-iurs!BTAUGw4szVyy7TpbH_$i)RXHhKCBGfF!5UqST02jMPsLNuG?qd@{^XP zhka7PRXL9I-1Zrr==+jh{Kpk5 z_|F6{H(Bnr_Bebx91>ss|A92){}e^#L-?RvVh)^s>G$K>)%3{Lpp+?vLt#L>WV+&Y zp~AR~KK-YcH_nurP(Eqia#KAz2ap`58@qF)J~;oooz{+y)mYY^(@D(NGY*NfXx4w| z@C-l-?O+JG@_v1JR@VJZ%De3`@)qwVpHp1zcd&nx-Ad*Y1~9C56^qcX zDdaa1?LYVoh!Q~iwKh*xtYY>f@3jtJ0V2-d>ja2LccfLtWjxPX9|F@8cF9DG0jeZ7 z>x6##MASnYOQ*$!eMX6}e+OG|0J66R%*|3Vf;}E0Cg)-Y3(GpcX(L$tBPls*nqQh0 zB@j*n3JZ%d`edb5=<5+#&rw!W|1O!~KV;hG{}hzl#DL~q0BOVNYP+&umN3EqpS^)V z5w^O4dSH59Q;0_al82%$z-fr;B#ZAw#Oq4V>*i?x=`Qv=$1%YMYGjRl@Y3mB93bMa zU^x4<`$3r7OTSsRk;$zWtM&Kd@{;wc_Bs-7_vSL63mtVsY3`cVcuoK`}$xz zft#DVdCm9n#x=_L@#a`LHR~&V;akgv^cWhHSm8Y$`$cJ=uMg*& zL~c3E{~ihxi&h0N0LC?QDn7zv@&SP%kd zR;lEQDubBy>gmIC&twe7x%~cSUMUP0dPGHz$=Y3FK0AXTPGBk$Vm5Yote^+=p{-IO zUM&rN1Ov1Nd9k<+To37Xt=-BG)ANn~mTL^^ba$73hBIdZI%(78D)Fae7}{`U;%~i3 z2F!I{D0-T~XuZvbp*9wD7Fh!lVYmZ$NNco*_l; z9D*p_{f=m3;rwZ~Tzhu|nv~Z@=ey&kOi6uS0Z}Zg`V-+_jhPG;=z3?jMK;0o@jPnl ze2~JuWq-cf$oPV5?8~qugv5Z1xNMtB9EMVH#j~X)oE^S+h6D)>oh~Z7qu;f|(?tqN z)&9?i!`4QRdVCXMuWg=}pWQnjSHh-2!&>DZw*5L!T&tb^UT^(hcb`Z|Nt1G4u1f6Y z35k6V%1S<~_&>~G41C~rCj97;uR-g7KWdbn#(`dmrcn_6*B5|)l0Tr$EOkKer`Y|l z%4cX;1pcD)P&OYbPjDz<_EBL0nV<&c(3xsr0GrFr(v}y}W~;P@4jkOO2P$wqSC1mY z(?wdRV#mX5wO~<@gRG~4`-`)ra9W6J^(pPqKb-2LF5mUl&WH_o5?+59-} zA-6=IvtSCBkzj0vf6YQf97M%Zu7cpl6Zg);aZgHW4hGj{$3X#q8LEB&r!dR>{ffG_ z100SFM&c&L8$L7auqONBVgz)O<|Hr1Sm&a3;s?L{(tQav5a9|i<}gI|I@iZC2Cz&%MM9N zpl*%qIlyx((e8MAlj7B*r)>1`q;-O&r!EfX?|Ib~*avMM;!xrJ$?(?R|1m2m{PnT( z^$-K4%CNQ3(oK@s?z;%fWwZ=I-Z~OoVOWK#sX`vs*b@>iBh1cRdWq0+{me459A~Jx zR9D@J;~hAh4#F5lD+$&EF~q~Ym|{n&86pH58)Il3;h%K0q1hxGEpmN%JssGY zm5TR3f!#S^df0DrK1eWs-Xwx&H@c1Yf1+;L{o=iBKt_%2X6gI5wB{z?A7&a7E|5+` z`@&>&KeDy#JfTFuYLM=8*Gr+S%8k|?P>ULU9cNwL&MN>8!TlUg?nU2Z2lHiaQu%K0 zZwciG45AGKaQHJ#)h61{3eS4!od#azay&Wf4B|L(c25|`3jdnfBT0&f1fluvv*@yN z4wHdq2G(Tcf>;&b0}WPzWcUJ%%24eUWebJHhbj{cWQHjrVB$> zbm$pAF@Ilz5v2a?#A-^u20nFi)Q73&?no^p?)|e+xUeGjxKa2ur7~_5Z(e3C5G`wW zT;S7bP{dR_Eu-LC8c)W;)KIzOit0*? zH130j#gt?^%R_U(rBhbinaDluxOcyrlBJsvxivW9)6lcjbk2TF7jmz91bOJlS){$A zA{?M&o3zOMq3ZE8eJ}UQipMra;V}Ij)Z}uPx~?!7+4*NJzyM&>Ya-x%rUvPnXlM`d z6j$?a1ejz55EB6|1cfEQ!q;qtn_EdL<78=+%$U?fraZw=XM{$FY)Z(i2s*NpyW+mN3>^VjLSY;xj zet|@IRD$LfQO;p#sp(+ey|+*N3+=mnXo1inz_*`2xXFlVe43OX>gnMpcXM3eZM^1CzC|+j zmp@!u%78H$mxx|(ve?i7TI9Nb)|K?;C611vWWESyA7a@;bV3vJ3x*X4AOoRn-&V&=bMgW}5zTSUSVku5tt5%@M~t}K{`pK98JLylou#K^7U;~W zdToSkABCY-cANoXr9_@?1pos%B4J$?7>0cMy(kM=w5(uiSoe`FxMCv=FaY83AM@_6~XTqlOI{yYjPAE zx1K7|ag|X9?l&!*5CQoy*`mH)taf^=%2~WDh;=ULCC`oS^mU2bLoTL5-_dmA$7KDe zMiXWTs=HbpiWp18xsQga&?Av?*}F?v+A`LjcKBF@B{Tu3~6h`QrDmZ+AD4aH!q+ zkdpOvgL$z1cJ&YgHLBJwi>;Sgxz>3n`0g&6!%i@oim+D%V|*-=XOyMJ`5`ACV4+xu zw&H?YvXtxpa=&#@UhRf@?UshzD+*s%KTuv=xe|SDPlvk9#E=DQFymqVP4LwR`1~z0&kQQWcq)4rxz*^Ix*io)5mRu$gt_MLO$?J(3My=|ULlf~VGl9J;_Zt2TF$aVFS_y7@kd`vMtJw=K1MYJJm=+mQ^%Gk$-?LEX$Gb%7snwV^_$DoiwzX=|k(&R7_a$2Lqk?>~c zpAEP2CKQ=4fYf%c&)8RT=BM2zCYhMv=X#SZz%glS;1F&c8)cm;rOs{_=*MC^2RwXC z2I3Z}8^XT$OrrESrM{XfKfz9StR#lksFn}PXnhW{103}tA^ds9gwFuSXJERL5^w?~ za2Rh6G9K1+g;QgSJXBqThs=mk;bii(F+hYKitQd%%vLs}4|qs088FlsabQZ4+#Ycv zuCLwi(rl^2reXlUA7l)Yu~Fhz)aU^+GEJ*q<&R+)c!ct({UJR+REpJy`0h7sVjiq+#K{Q9Gg`oGUr_+^|E@5x%*2IeyGW$(|oEAmdIB(-i)?qacfD$zf-lGl7a8c_mIk4lwaKh_KdCN{w>R0_ZVC0K^ zEueTF8fGsZYQQ=%#i@Hc7Dl;ilU6ajVdW4mfJva2DF8>EiAoh0V5S$jDvG&?{%X7{ zKJ->mVw2M_)TAD0!OMR(=t4k^Ekjo6I2l^Q(^j-$#lh%4M2i+qDf3uI`!104J7qPm zAaZZxc9t5R2*#ZC9w@!cGnu4-T83(eC^Dq;To%0GLe`m)8SGClA6A=Rz+9R>aEV0# zksYkUVZm2c?-DhH3O_{P$e^;|ua1?nYiJCmBqb$c3oU>)%K$0eWzD@)MB49d?0I0phaFZ$i|Jk$meeqMbf*LtSDu7Rm;m zE9r&M*_oIWJSurzp~#Io5TW0Mo%hLJ7Ubo@{;0spi$tW6fo6~56h!CSU)&Hz87WR9 zlGce2aLFU2L1k%-u#Z9um5dMO_OOXX5X^WnHkI&Uq*Ku$W@bAlgKVuP?*ZbeljDi_ zWBinRiMm4si!7Yz#6cJ07?V}|rWZ1PRAPywAq@?t>wS_Kl+?ui-16grh#3)2;sa+t zue_-4u^wh2`i!rr$UI`jp!9O4!hGy5{&c%uk!VEL;9h=|w=T55k;7pz$esJPn~?2^ z?Be+8QL|&EB&5STi&Xep5qS>hc{+p;ZF00e^?+Ev^P(fTBVfB)vz?d)G z$H)0BO5Q(|i$L})Mha6hFOnXa_MpY-vzzG;&zu(M0m|#5v;7%!ep7B$@%ovGe}KaD;TheKG4_g*q?H9McwN+C#dbD`4OU;y!21A z`+Jw1XLf?$_x@a41@U)AcaN=2%MJ4QdlLqee&KFQ_a~|{NOuA?5t2{i_@xy@*Yx+K z`+I}9gsBXRrqO#-qV=QeiJz4~hb;DRzm;K2R>Kc&<#)A>;>E9n>XHF+R720q19b*a zzIc%Olt2@M-2UFXR4=1X=qe;1$HdUInPcwtNq~S3Nd5imAphQIe{Vdwe!?k9iK&3A zJ~c{3^jhjbmTxu+4LR^=+mqQLW_nRT;6n4ikdr@@hT-F-e_L*6tEIV)+Y(w+$r}I*u%8*K$ z;5AHPOb`)}W-Fnh)dmJrNKfFAukqhmiO@}Vg`jbZlb#O=0X_E|yR{3@zo?zakh%um zrSi^XDl(DS8n%pOxbM`qr7=1SYIlKQt8_AYP0m~^IVrwz<`zGk=N)|2>>E*_L5%zG zyjSE?rL~k<0@@(eI-YSuy@9TnJ+4K7Cn|SF{h2uJb zH6K2DSijjEDpnMK2w4z*U~C?-h*u2UE;(OvL1Ml*QDbdYAeQVTwGPMwi7A9?4T(Mk z+ZxU%iMMA64=D)aoxBbl89EO9I~@`>=akq?*|+3CtdlM^IeM``b30!=-_#JEJkw7Lyk#AdnCz@bqb6kWZOP!W=A#3547~*GUq8C zXYi0}3efvyA0{!{%F_g3`bTICcaoc2Nh}FnP>`YvY<~Dt$a43Gr@sM#{AGJEn z)m$OUb6WZKAQ|7%dLTT}G&{Gy^(65fEHomN(>82X?c8c0RDI1^qEu8eZBSI+CH5oz ze)tNC3I-}rd_&WTL|7sO@m+*}t)UKd=5idAvfLY+uJ`e&Txd3uy%VS-G1L2HF}&@q zX)cP*XUams)LmE(AXzVdI4>%Tpb0o{A_VgEb*7ZYDXf9*rKkLSodMMnXSzOR)0$xs z&B7a0C)}9YsP4nFh#_Dal}xqM`zl$7oVG8bL003RW_*{SP}M;mTvna3;SsxdxNmhy zeeEYjHq13Ur$d34{Ei*1V)<2yHO~Txug#bh(rK z`B)u>-(el9gaLa_Q_kx|*pxub%a)!igv9U4?R=MXkUn?-D1q6GYXSX*-kJDBmg zX6kbh;&R7+r9K;=liRk-<^herfWRxoM-mgHF(gCkU>S}qd zNR&8IA}Mx#?C%zfiT2OGW0im=dI3JUEIXaCvjS(%^9R!Ik-y2CIxpkAaH%zRx-h zOGWB$l3eNU1S-8nOF^s7MfCC5SsW@I^7evnkKP=VgiHP)z5e)>ZYp`6+0}{meWU?% zsAG~(@qR|-V03AM`@R!0FiKV9x}gpfOc4rjA*QhQf-{zyK5X!GmU&xYSV)%ft@`$m z7&L&}m|zr9ka;G*iI96vJ0@AXAF95e7DQ(0gX*bu+$P;o+>-N}vx^S%B^Jt0!1Zcx z%__Nps$6(r#6RP}^Cckr6t>DOLWi`hT1-C5w0I7;#G2)ZL*%vK6rCh;nen$&e@=Bx zjNfI>pwA+PQl0;CU=tl{h$fexz60TBj-Nzx7?@&gQy6e8vGOuUKssf|v}*ePUO;n# z72#drXiIwCI2fuGWx=dTRYT2auKrhIX5))YDqgXY8hHdUp3=j@RL(?XrRLVr9>OuqE&p$KZql@n1=G0??H)a2riS|5K`T;rU><1D#VSDx8EG69BrJmHfSD6$`6mEncZcxF7jP zXQ75cIk|x4&GAlPcph&hpjF-A^)OMD(2pu}_Q_!g7o>SLuo^CZ$1ZLDaq^%aN8 z3kw;K18>wy5`bOgLn|)DlxKeKfz7nEo{oQA@{kQfVj_xUbLxiXjE|TLtOny#LXY^f>d(-ax z&8!()c3JSC5>R1qPt2FXV>*Nx;@^Qcsuq+lu2+zbC;a1^W1b`)Qh4!i$kvUiZeLSa z7SVk>BC65jXGDS?7)rt?BIH3rR`qn1?%-%Z<77#Nry)i8j58(Fu!5SK=7x?;rcDVE zca5#TFK{ixEB*tKm>$TQk>lewzp5*IkoZ<14{vVsFeY^n(GAm1DSb1nZUXs`=VcM$ zE5#mQ9Zi)k z0AkZN-8#bDX~9uxl9Z}i6a$fQeaw2O}PialYS-BO#{i!iGlP_=Xrv=-@D_<&T#AXX!ocZO}h?Q z*12c${m_NDi!0kZ%d5n(-tw<5=LafPfM#l(3tERv^RQ%#dWKfF zhH+PN07S%n@!kwyd~b9K5n+9*eYTmLcb0)@J9E1~vv$$2a9+o_CS1ZE70<8g>pm9dd^L%*c zBez_nPPG^Fm_+t1(z>I`IDs3Jc#*mDB$|{X2Zz8Wx%9A$ZTGhFT2)`-Ys%5$=%lxtust)BG{LOe>PEvJ% z(sxoXPSp8wU7eQSa6Y^lFLrWa+*pgn${&L1?vc)3+x#@(mq2XMNsV1OUjG>r>WpkK>Z$FE@*!VR7hj+ zfm0UN7z=6ORH<6wHaITd?ZF^I0`mU2BF11rP$T`^ z6~t^~=Ql1`rL(~G+&vyXS)J|C-7N7S{+CG`6kc?;KeD5GGuYkRlDea4hGY>*KdM^B zSzvLbyYfHcm?pYtKsN~@3-0fGe!|(IPz^XAm-BQ;)W*|0`UI+*Ze?S z0vZ{r&yG(41qOEm9A+l?k?dDtvG4B+o|u_kbnS_k3lWzcHW9PW3IlO?oUiWQrLB}f zx8d2Y!|{RAVVkxo#2kXt`0iqO_qZ3CSWA3ucQs*U;zD^(A{ECBcg=!<2#_hKgQ3LkDY3 zJpSO#96r*>IGN^iFjsObBdf+ahQ)KMSxo3$sH?IIUXQ-M!t%*oeNoP{YH*e>(W^S3 zPDpUr@x>-mybb9|l97Vlc+e;p4Pe3!oU!%!cx!TKRn|NVUhDee0kDWb%$P%eNXuK(=l1k9WP{a zAGE1GT9024FPsvb5|p&7?gL^I&gK0IGuoP@Wxs31vSF=GnVYnK%~!_d)D*Ir|J9_Z zXA9rhduDu4R&vUGfZ^CvP=QxN-b&}Y16c09wvrI|^9E>6Dl9g7&J({hQyw8;@lQeQW$L@?nytQmX?5;fKaMxUmx^Krx)%fQOcu96)_xXPz?u8THIQ(s z{}V)f#lf*VgvmFWIo(0CMw#Ghz;Z^`A6q+14k8^}B^X@YvA4f$YWiz-7>=#2->~9U zCFF+g+B$fL*U0zGcpSUex-ez9)pAGOQ_$DzicZ^)Hno?-W1i2=Qi-?VpQRk^RZK9( z*Wwr`Y-ibb$|SWL7Ov;sF9|`ls9fIK?}TD)yo5Y_H>*~rRn#f|Nr-9raD;Y!7oW;* zK}Q5`!>PTyu-rj2KXrkM!$}}-qVnQXTzkGAzFIMECuHg*pKiVFwET6B@)Hn~q;B%V zs;1{51EG<}8JD^MTZZ}Z#&#;gq*!pWbN-Payo(8Ej!#$C^vBvJ%gvf)qr0F1$E!=T z{BzaA8?QrTmfr3;>WXfn{>$G}_<0B!xbMHc$Dqv4*tovD6Y*AdY5XQ!wwm`x&I7>hVQP*;BpsuKv)#m75PcvvkCXDV}VMWmUoT!Laqmu&ER zBI);!_Ha)re(fkhM-CJTfkdm%AZ#zf!xbD#klvuqy zmKDaj^=^*>qN)_kf^~K6eM>|rXQ`aP4*0geBPR5jlens4JIQ}mopRl5#iemtOrrJr zMKa3dE8fr`&vyHJA$T#C*G;KGMiE8S?UK5y0{HQnkJ@~yJ=y#GZj>GA(R`n}crqJX z#jRs;x3%fuYc#1Nmm0W^pewtd+*u72L?5nO)dx{T^RR!#M!nvc_D9vFtkM>du6CuT zVY75E=mM?e18Y>53J|?hr(W{Aj+t-j`3x__`=#D<@9kImObKl;N2UI*$6Bq_H}Z9U z@GE?BF9+Jq@TG`BocHuLpt7_cS|C0-T|$OrEA5Y>vqq5z4?o}FsZC?srXFvxmF58Q zZ70*@KB7$$(PL*!8Uxa^gM5&u)fbJAE&f(sx3+I&+8htg7vUz>ER+sY=aynhL zPgp35w<=jE52>rZ=#Y}%Bvj9SEo&U5lAV^XuZnIXsWuY14w#VP_{iWXmivLW?t}cM z6+>5f!;W|C7ynt8Wd%W8w~*vbpL6H$d*&9pm?C}lt&+&!zZfWK^> znHLL@^A!`8YNi`m9(?rk%weR}4)Xe4WhjaN$#zEkTzgAg$)PLPaPj*#H}X&0Ouv>{ zwffbsO6pnR#V|0kur1P2R|}) zEddg*uj#%Gaw@gQIC@dAb^@&;vX53Wvwd$KXUp$9o=RhO4}et}Q}T~qR1#PaR}Of7 z@tl+VA8m3XPA7u?$~huls}{qZ*{W6I7e7@eS@&jDuvv&)3>X7n8Ky2pkRO+>mzlad zG;8Sa!y146nw^bR|IOf~b9GXZIO+oC|8RW44iBLBlo~ui#MxWJiQ_>D#$R{7V&|mn zq^A}PmArN!FXD;ws6VE@8Y6h=Z7AlHon!4_DT;w1F0qIrpAI~&bRRRWVR;&+8y4Hv zB$J*7qZB6aa(9Y&Ki!8*Ea~V^^o+-JClFf~jb4vwZMPXn9{bO=s=*J#ERB9k&$e3o zYEj3?xMrb{(WV{x!lJcmjTaZz@=hh3P|bpqwb-x@u>OQUKmMig$8zcTxY0FxsH7}1 zaFO@je8)D|b`xb=OL(@RJqi+Pc}i8s=NnPg5{|R8I8FDv`c@qvTlY zq4I-2S?SOtT39-eUD|HkwBotySF2|>=u6I$`R*cTcY%lq(k{84oX3qwL zl2Mi5^(*$J9?5tp^Rf;#uHnhByV40_L&U|g zX@?o*yO>^M*HehpsB>;#*e4h}5X~I@l^eJ1aP3c1)q(ZBKSp}oiu5ZJ!?9<9C?Oh6 zU;!*{Qx8T$bODbu@!df@0taPW*%~{7G@i0MG#$AEfaG@vGA3Y<50n)~ zi{>FvYwM6q;=lxjAs$xJ@;>cK1h9Oi7{-J_H@;iSch2&2_@#V$p@Qs<5h0n;d{DNL zOMyk5g-UR;d@$n}Wj`6sZ;g>rhRh!QZyudV9KGjXEh09qRlm6H38@IxXW)dn6+`9c z*zbYl^#{6ete$WHaT)ud(&EHw^9_QH}pEi zE4wN_M`K4-#+UTX!_H{M!~SBFeEhQ))HnNJt-<}{2W4RXgU&POTEUN4>+-_p1^jRy^ufKGvsjuI-_Ft4J-A@NF~eyr zYD{avvT~`lCIs1H`4LQ=Vv`vqfqz*y@H)jd-uu1(4kntqYwO{rH)JcQAWO5}{Y6*D zB75=;JQSMN$Z(|T!^klv;h?_20qUM1(bvh_uIC{zW@|m-w8*%wYCVy+Yrp>Bez6<_ zhf}3py~=o0+*p$rjDo~6cZXzMmEPHnEy$n#aSXo~_#z`I*m19Me{*co-pj^$vv)45 zJL#K*fx1$~;^7($jk$Ln-T4{?8_`(lMM?JOC!tb@ic|BwX6ZgrTuMVbA>IK}y zzOs+`;8?%8!~McVA~HMBxF2h5BMNKGy`-O*$$!~&0fPAzZ~Sua0HEx98dkf=EZcQE zzei*FhDj(gDX3Rp`O7_BjACiJvFY~bE87mmwB`)ml$z+AbI)6Ws3GPBJzkb1h!a<- z*V=PGy?qwSb`N?OJx4u@dAHifV$}~7I{Dz$1B2e~3V}N{0^#(xeCT6H5qFwz(q&Z$ z>$XEAIe;?ob^A}WXau0$akJ;167(1*Ph`|~ohO4fkJ@#T-=}2M%bFjG%f2M^gK>-4 zt5YuSKChd=iAfcgVkTZ3A8%-649|z53rV3aQ6DBz$F1j0y#+Tb-R0sPNtB4+9DC~) zB)uC3hdVFM&xBPnT4LUZ>ADu6JF}w_vbsd&`tb7d{`iSJz{h$EakbTDwNIi`8PDhr z=j&Q;KevMT-M35;fZw_!+=X3V zcVHg%9d9jjYeSVU4`XHaJZ4klcvD_{52ZgjEebpN=Et$S`jbXW5)t zRxo{Z&Gq86TfK7@2lZy7WPcIFe=yuH@Hv{Vj49wY;Sior9cgeDJT{1YCsd5_p;yZn zC3-+Y#u4IhmTj#Hab{F=f;cd=+CL3bqDKsP{01T!*e_-^Bk6_3-Z_@Q-+G-X^-4zM zi!+M`eRH>GD+GnGPvRlH&_izo5>hD!^9)RXYB>#FL*lbUQ0}J^VI=Pvx9p{WpRU_| zd;b&H7A-dIEth%BWL(s!6shDBb*Odiy1v$(g~I){>;2mah@-4a{E*9447rHReV^(I za?~R!!-Bp1o>4yg`~lRAi>%OjUaoMycpff9ah|{%@}X25Yr~BK$#ETBzUN{G{}w8Z z&jYjr2IF>unfN1Rz3A;UC@ZYHwNaxtq|Y@Orx$KnNDu>ysqz_p&Z->$G>(XbI zHMOM|6<;ITFBDa$VIP{D@sO^k4)9$or{h|3%;Vv-WE4kBcfY8ajhE(|_T9y>Mi2k- zBqc`oc5?i+=e!0o=z1#!c)HAe_1S?q{y6TS8r=mwRwB&3{g^B2oArc1=n`_u?H5c~ z@Ggj111(G}9!wwBO4R0kdmhI-yf8MD7|m%iU^Bwa4M_Xh%Ry|)!+&_#YVn+TN!;

gKEaN&C(1mW%e#s=0b-&cw;7R@EoQ^DQt!v$ugXrb>eJjm*e!l9% zEVJ`yQv1w)mOsa8@>g_&qB|>yIm#H+g!r*}%=eLuef`ktRbP6uyv~OreX}z~+IyN- zY7xfRZ|7HomdoO*AzzHFyY>~|^0iX;Q$QyRdcgu@lXhC{ zad|vXUE(2b=9M_@PG8X}USB;T_P3e2&Ky@sF zUAo9|PgW0;O`LwRJn}77S?_pqS%wodk;2jCqYm@>z^uXSiFv>4tMD?}CW(TWR&E&2 z>OCt>9~p^?e4AgrNXDH7?E<#s`k8JQ_!qG*$R_7A4x8l=T>;Z}aBw)NgV+e&o#aa~ z%Y=5Wo&pF~j8#eBb!2%awOHh5Q-a&wB*~&&K97&mWhGBNHm{O5nDf@!Ejti$1S5Ek zK3*$lo1p8FCy0bPYGE#_R=n8pxdCN8&TN&FShIO71Lb4ojetSH0;-F=Z>0>4isjG! zwCxr{hKj0bc1KT)dE? z&ta?~V0-k*ow2^P_tH&20Uc>V2)Z&G)l0O`@H~ z4EHM(W+i?4$#-ym%T(HO|EAwz=n+W~e-h+1asge50&I!X|&mGVL`2ITNw}{kO`V&naz;KI3W8Ck$$0 zCh#}n&dlcWoF={EcFy`*mbT7=8wqWpAMO-Y_jgMx?!F>vl;^|r{qCOul#kV#4@IA( zs?9vELBX&Q+x=FTGu+!csEJDY@xJ6vo!-{H5pkUIaT^$?xbXhG#THf(JoV$ zlHJ^ox9*(?2p>-%{&su(5Yv$-aJKcf>|2_5gQ$DCf^d@VilY=DnSm;eR}fy}CXKCA zv-NxNC`uRG+vpwn9RQP&SOZd9ok6%F(DlO(O37$x59RUf(EdA+U4*q61~Wip!fJlG54^Y1bHC( z0xPEguiq-JZ6%~RKI~z+C(vWnbHRKmr*84y><2h!EFDh;;~v0X*Ps(+aZxYx_Df<2 zD8h43{uHzOea5v=d|wj{@yS8n?|&Fzjs&~qp}bs*bdm{uaV_{_xAL203}(@xhzws2 zo|sGx9}I=M=hru7c>F1%So3r&?8@3j`m6M5loJ$94+o-M5a6`&W^R71TC$k$pf!GE zOYmOBfdPm1RM5P5gHnz1=pZw1phg_N_I=4L+nnA2;xdC1UfAh1mw^rkxr(M;{s-ZxlLspoy9Ntq!bEZ!Frgi&_Ri%_!8!w^>d=&ffK~H>!9RTQO{1?&#g^zqaQCqjr{` ztC(cKNDLKJA*^Hikk!gX9*hYLQ7Ty@@es88V|UO`asw@LV2vIj%7-{*yeT?Ib`A2c zaf6Um6<=tYL(t$5auErtI?c;bX%3OSeV^7EOc7vv~5N= zy#K519=~OTqp0$klX{j$pN|I8diOLd-QxW~yv89wwO*oUor0f}*h^L)-~Y!tSIUc) z@r8E(m~2stQPJ7eH^>gFCq49RMIO^6!fhU@Sb~>9G~AV*-<}f|)+3eIpOAAod50AE3Eg;h|aLkF$@!Vn9|u^mKrIVKlW!LG0R@P+=v&XmpC;0{@B1=pNm+ z5O{TUFss5{kt*iuUte7PbdTxAGB;a^cibMi)00-flIr!*B&>cAkMT z7qOvs*W$4Nz zLl?^aPrkLIibP|U&jZ1|BNc?%_snksN(0);wuJM!`0>ST0^U2N2mPETj+zkMt_AiQ zacken+;cEflv%`)lR(Adb1`|lQq*3KJWF;>^2-GyjNl3?WbvW@3W*Qp;P1&}uqAl& z%DW7Fd2-E9=ke4-*+Q3pRq}e-dV^dPl1MxA{V7m3pX=4BK5DDUi?PcrgHS0XRUCVar zRz=;o6H1>~)s+OOLZd|4Q9jiENtJa%bd>e2a@UDN7}3vcp;+X)^~;@~&cI0LoeT>r z)9xFClTSLI$Fc)fOZcgr3^v2C?m1DP7R*4_*8X&f4o=UmC z_~-$KzXp7C3QBFucE+Gavq$^f265Au%rw6&@eN1!)@i?bD{x$YSzmOA%&=7;{YWOT zyc`VkTztfC^7`kfnCucVT{b0+;537=CG|=Mm&MKn#BN>Ea?P+IiCVmopIh@J`g}EG zs7DXqlVD$G7WV$~kLeoOtmlo+3B-17kTKL#(i+|mM{z!uPHQG_Ac%L&blHaNe}H=H zv|h@zmE$uh#}JN4;lr-K^C;Lh204`BX(2=M;q&w~s5FM&)iV z%_E?ktZ`0GivHg9t+tf^bztOgGgKtGCxS zlEKLqKZLw1a>0N}{vN(t6v|IFeZUVsAXWa>h!MD)>mV%U{{d=2mA<%*8#k$^kA>b1 z!u_hdy(JrA%?~)#{7Yx9XKS zqsolRe2416I?*&u^=yOc+=HTnjU<~?=Kex8qs{VVWf03e-_DIU@QU)SY?*tdux}sJ zpv%;@daIpsi($R$d~cQ6(ej_^%1q;KeS3MUkBU05Hha|QDsmqPxYZMM_&@Q9Ph_sU z?z+r`2@~9JDU>GZYJRWDZ{kX*IzM}-X*GYD{K&=~k$wMN>1>ysealM9kn5V?Y8Cg7y*?iUI_+x3> z+tRkT`PR=A~;j$ev4!ggkvd%hO052a`` zez%uzrOVuC^LKp=9XdZVdh`RX9(3z=wAxC(eujLvvS=@D<>Fg9VcLAxewi0QQ^9t6bszn|Ni&+=^UdG>**>pu2L-PH!n3C8**$ z{19j9L7#oogST7x@Pn?9HelgmaGo}wH}Xpw*82rcp3D)SJd;0qnWW857kY!Zy?YmH z%id|~{nJC-qrCSNZGJ(SIPk{!LeHC?kI~LF8jrz&5e7JE6{8I}59?UaMtmMmo2N?^ z)WwHJ!x!=`O&jG!-U&OTwm5z( z>!JzTyX(!iY+t9hZ)Zb?pC(N66<&pWOY^mIWe#n9#5YErlU3)2>t@f}$bcMo@ z{=Cf*24{suftkb4!@cm>w$1OD8aC|Tgy(q;7r&%JIa=vG2Zr35LKvTKa9G*~Gk<$S zKTqg<+CDx)IUKH9JPS`n3Z2C@KZ{ExgFjv=0`>Xbv167LyXB5{ zgfdU)3_ouZ%EjP?XtVglv8hChAHu9G4aVExylDhBZDnCXcsbfA@4{v7u~{b|jho-p ztzxu8L&uak_1D)=P7-(P#@2BB=)iC|3{xR~&BpVEI!!0Rw-EtzF63Kv%AAc6)V-0S zZGr0Bh2l|n@$gPZ1BY;1LR&2JT#>o9VC7b>K31d5^ORKQtHTrs%wet_0ci>%R32mx zMz{Vx5Z@T>yvNX?Khj8Gq!zdS*>TLD#B|tQ<#(P%{#ij60+)h0?6j;BE*+oW>HHHN z{&;^cbZE4=ojacds$T0EQjU-6yC2@prE#0Ty=xhM;M2xvnJtt%r4$xgku-VN*Rv~- zdpFkV?bkg}GNpJ^3I_7_z!`zy`u4@n(q?s>{w#cliaxfrd)Gevt!pcmroG#fHaJSf zF9j_^~mp& zFrKC~XDb4WsD8mY9lO?kH!W})8V4>}tp20&vP08oM@WIt2so2cYt%=2JO?N@>KkV! zu)Q|ldH3!68BH}FqwntWV+!*njjo=NfM#p9wmQi%O5vbbe%mFZf{rR^(xGGHN7_Am zP8Ds7PUKA((zvnbD%>NNOkx?V`bm-3T;)rL+`!XcK9YIbiHx0uA@Af z^M|}y4|a)^ZKKy8# zDa)-=B+qHB@PE`%_`O43Uw?$cae)xHI!b6$*Vt+GqTX!(WC!5Aj-G0n!pI_zuP(}g zF0Mcr=5r(9Y*qHNG~_-@jqsl~R_SZ-)|wWcLc*YgUUGx~pme?c9f&uDb= zaCQXEfmf7BN0qVfjP>hxrSfoy6bPL%>f1W^?zquVHo5q!X%5h}>7owOX;_0C{Ub(P zs~-A#+04IK6@Qp2{F-dO4uqb%-&C-B_fjpw4s`kAo@%=-E}PN#>U0kHkpfo_hstB+ z%CG1E;i*n}F!hL;bb9zqyKdX|eP>N|(V|8vjw`iOZ%8t3+`T#kd4ua1)zyttt_JI& z9#>n5fZvM**4p;&U9S<*t)i7Sh7Bfvo<<+hMGiO)bWsIDMQ3Xq^UHJ8i+iWG9iF7o z$IU)dx--KsMpnDQx#X(&&y=5^lit01f%4p-QOIkhAW7ESpsFGP5p|0>KvSYlPEnnF zQBzhwa3hZuE54{Txo=4EpP~`PIEA53va^o$ks;si$>PNm75De!;Ax>7Ko7LrVRGeTIHFr7i&Yd&eyP@IuZ165V16e%NT?}F!8rBcy zNWs6xt$A$NaEmrPd|RUmwq8G`a$KZ*QI>4EX6kXB{1yl{L+@3pyVLYck|~zA=?v4A zvg3BXOqx6**a``8ItxrM`O&C8D(={QmC0<-*_?~qNNDTU2eN5cn%3?ro^(XmMLEzZ zBM=UEapW#|YIrZGmv@CmAk%f&=|8gQXrNT|PRUx^vs5%D!pWcU*uDEVYT$d+ zz$a!yA1HY&n)xmQu#o`*KG8)#-_tI@< zyRcj;We3YpB;g@8_Bo?icyk_t14*18F~>`QrzIAOZw)Z$4-(HmhH&lmSm$1yuj166E}axL(~O_ zB}oGq@yI{0rnK!Y(|m(A^J_|5`}mgTYnpGZ27pndrrEjh_K15;snIZDlzWPX8!$iNID(&6-Wc^;? z$ZBYqs`Oq`eKaaPFWb{r4vaEySvxOQAB~&UsgHTu(sIVqt<<~26v*)5!<_(Lv0_DX z?z!ixaqMu`ZbKykGedlt#F3 z8oX?PNqFpW0wl!O655Qfg=jO{!I|a@e#X~Ad^1=f!A9JWw)XH9T!m=2^gFi9{W*>y zLq6iRoLs2(QN@Wpb%I$Rjjjx4hBLK8>y};hR&+6APvZipI4&WZDh4s(s4jryA z{hZp-k%B+3GIuC*|3bbNtB=LYoOPCE>YQK|dW_a$yxF0hJ4uA;wpixyw-{ebweyyg zIptocEm_=_w539{5f9p`<*Kege1oW0Ps-nDMn*pQ$xk}vfw^={pf6dn#N`jWM}7MA zcES+GVWxu73g0ky(4c{uuHrz`d^J)oPI z=rIi$G9)MX5M11fhyypasLa#~=^-@Nty`B%8}r-!`t@^Rfurc`WIgeT3;lcphi>RM z+PQ_19;X#1Ryi@_VGcN9>D3x-gau!Z9zB(}VfyAGpVh)K)kUkTTQ5IsD)kjF^0c9Z zu%K&cbD*5fXdn=xJ;t3~&5G+R@eYBLoVPfDyt$Exkb0zCkvEMGf442;u z-TiQq1Z5w`VDLZrJPx^n|l@7~lr z${OX2@P@YO)2C}k&!IWmN%!fepU&m&m}8D{3J}_%g}X|)XN7N`6Hh$R32}Hzn8zM_ zET?=?F2JFiu$1Q=ciiFfLYcxdZlN^VP%ii1f4{>=*eO${INpOdEOQh(a9`#_a(v~k z67E?cEcm(a!<1v^l{r(Zvo*!^o@C*|1uByttAxj@%*UzB`xY#7IwRCao5n&|G%fSd z3FU(8*6rOA@NY?G&3anA`e8D6?k_bW*_%w7bguZaUA#ENbvwcCd&uBfcB8Dz|Dnx!!ONO@D=iO8%2CBT) zs(jv{y8K%S^6Rs^0jCQ#Tlx13)8sd#jo)d$HZ61N0&y9!3|1QJ6n}p*VZvJRZ;k5y zGzs{r;vwgqwhHd+Yq9zm(oWi*Z&)$jDH=>^@EuSI=ei~j({6~ z+8b#Vb10BzkBF_veIO#V*?k-85ogqEb!kd5<u-g)OW!%rN0 zJne_OFm-hoDgRqD{rY|0{jRSse9uXHX|uG8(Qfckd@aY9_QKmkTe-O9X|wt^*_FfK z!GFk%AHObJMtxN$hPdxEe~fR1(k}%|l5q}Sj~?e|CQsg%88PCvEdC>M>9oDfJ&w_% zAIgjyw=9dVdA&0F(s|;y!OHWkI&I0)FGU++O2OMpdpZ1t;6`^LSS9#c3a{~^rtbqm zbE3UiLqHo38Z<~laeq#O(G1#7nN%+a4O{OA^@P4c6rtBdMdX;+ZenifQn6n+-x^dqJDS1b}NyA(?qQx)d zYg5{2ABz;?XtklgX^rI?jds4QG;Y*X?#ERZjus8i<#3Zm)A)Ww9j5L1%K8UW!Y}B3 zl%@htP`moMBd>dR+b-{0@vVZ+(4idzpK=Qi^Xa;j6`Wg<6Gw`FW2j zAP-U6y$sw}%`l7#jPhve*pA3zE|&mNfqUy=c>c5&iZ9WA`0uHaKCLa1XR3i5rUtW8 za9XIJCM(CRun{Oy6GvgR2VO4iG(1#vAt8iIhXuZOFFRp|D9;=|+b$lC&PHQl-h@wH zIAwwERW5IZ(hK94gSUroU75B_W}Jrkb2P2^dDX}MnwI_FEWd4YM)6@k0b9p6>iKhO zH%DnI_$%sYP0)ht$28UOlGe(umH!E<2T!^5r5g2Jx<31h%GmJ18ciaF^ljCIQMED?YU^~qdaZCJvWV&{}t257z(T!~ah`ymx5l&;!Zx<*!zl;qqk%(`G$E7f-*sx}y>l zcH5M1?v-E%)MkxH{8nCsMRc9Rfx0@@C}elH{~M*?SE)nv3khV(Xo-9&mtti}thPCS z24tGnTHd2NFj9j58r6sO8kPT0O5|iIfoFu6wncj?rod=32!|wRUrh z{8-bRtUSzf-|=7MKq(G%xb$f*AZcWbG+5Z3Eyc1x-&@tgK3j|GKh_STm$hL2R`ra2 zoK+qOPPXZ;*1N;WSDKySXz*O^%U*p(BWu;@y%`ty7F|bkfT^Dm64uYTgt3IMeftwx zBC6RZ^YuNGc}a?DoOT$VBgOHETlntR?+a1}{}5eM^e{T5UeLCvJB1##p88ROadQ6u zDefjshrLg6{V2B@%FiVh{~b^qsAg?%A)^LK88F&7QVQdn8fi>WB_1FF=?~8B*zv69 zd_N_j`HU(&J)>m`yIoIpdU!1)x|4z-2v8uaG=(wAecO$NvB5ibbl^YEw!EC3zUr-M zr9TDbK{-`^^pRHxTa`9D4o5pHznF|4HR@Yhzc@-t;JuCx>}t^w%Vf6djWXe}sUg<; zj+D}PLepU1)7rj6Q}c4%QreU2~rCU*td$4%C1Gv4+N*En}OsP`Fsr0nce} z7n9b^#nM}vu8Qp!-+kq_cn*A!{PR;1yLZ1N!PQGjc9Mj^JT>~h%0Gf4Hu{dK2>Fq3 z-@fNap|4kc{kh745u%ktHR~tovlcc$?P8qHpPZ*Q^8;<~zF5lOcx}V|qSi*dLW3WE zA)jji8uZPA{7R=hSi2cE?2}r%xkV$H8-q4>Fm}%1RCKN7Kn*C6jjrKw``X&IZ&6RK zX%^UNg&7F1JPvLF$)0`O?%nhCpYu56%*kIvltem9NfH?pyYgo};e^P!sm5w5*eFV|}O?3=ZaJw#JSKg}ix z|CuJ@U2R7?w23XE6^@N397Y?d_SM6! zE3RGp5yc%Vm{*9^raIz3usF~u3WQVbp&h8a;KRfT7H5sg)({UK!B}j2FC58aHml)x zlY)In-@7FU4tI=V>LkxI<6V4Lo&#P%FlC4W>EHkLdUJyJTiK*oo9jqNLxCoyrRGWz z4A=J%O-KEgd+O^)Xm`~|WWnJmx29NM@6UE{US<9IZ))^9D?9RdN%64CVC_wx(M1ll zfCC+-K>XZmMm?gzs=Vu+AvrT2}Q5eyD)yCv`fK57}(PB zTLVwW1z#Ay((n*>b!Z2xZ#KPpouH|rB}Hl6p@z|(Qdr478=uEP`v zndbl@$^(b;z}O##3e9m6eZYWz?u-a0X&I&Pz+~bR9vpn*M_jW70}iZJazN1-jWIqu zbVPVg!&`yjkD);v;W-tIQ9i?s4H)8E+ME#Qlo!0Qv>A=^7`!W#d6KXO1Ih|8-7?Qx@<_`dYv<5WE!SZR1R(=6 zKm6ejJIl6MiTu^Cex=<+{WJxzRTWoDf}1w0k~ck_jsQaFAmAu0p3vcM72gy>2oD=5 z#=_E$218>7hgrY1F!)pWq(MdE37w_Qo5JD=ox!1@5N%;t%a;jXU`87>Y}sPnNPo(C z-@Y{x&J!GpUAr(1UzjiDXd{ndnnn};HX1T30tVx4eD$<7uh?4NOWR-lZl|3)+4`!nNwz)~D|6#3ye-7nre)rguizwJIlf|@ zcjwL>dKQLkRXjQ;dsN@4kA6pA7{F+w>6A5}msxUfQ7cO)?a0DnO&*^ajC;ho|=eqns8^2FE z{qE5>CJtQAKz0uZ_W|!oq~6FqdZK> z8*NxW=I#lj4HNg6Yqz{%4H!jkv_bds<;xwf$P2XF=n8&;+mtPLiD051x_M||SZTIK zhk5?S#zx0yaB-gpc{AED^-lhbw%RW9RVs7pkNWW4&eWNu?Xu1Gf6CjL`kkoEm#EAa zl`V7l#Hft=44-gxU<#MHua9&d_AIrRU+j(b}xwS=C(JK6}stUv^ zJ2YQ<>80AQzS1=qyNC&Y8U}&|M@A7W{N|hZIGB$^hykb3pa`t-rQs+6Qv@!e{t=$| z;I{CE+j{E&9iG}=jz-{1x(-=^do0s03%;B49Cn}N`- zUCR^>QyNDI*DS%Oi7yBn6tXu7ZFvJPiWS9Fj;|qY7T;)xR^xq|Hp23tU~m?fw4oh; zXfH&Y@wFUn@TC~vq0P$9Xsc|QQy*D`W+MZ4WgMyRMar92p7sw^-nNRjP5EwogJo^e)}@_3dBZn7KV7hO@S2RYbKnM&I ze1uCo=)<21LZwJM@_;kJmWC6LFf_!pg9rFq8fm!s+q)PX{Ke9@`#`vaDeX$p*0=A) zx-0uS9UA+TMkF^WDhI{nwbmnh)3}JlZ56y*m3cM6wtt>1J!iUcMt!zUW z-{x=O(s1*)bkgurv=O!xUyUD@MpNAV#h1uF+@$*YX$8si-r^N2^KyJG zMVsNYG@8;D%N%|bYiFhSTDr_z&Q~j+bh&h9nOok{G=*)aJ@;*6JEp6*fTH4SBeFjg{##9c94>)l`+VC^l9WnZcf`&%Yv9yV2 zJxplBv1ND9p0%1PV)qQa`tkBhq}0bMPte=LrAzuo8@R!1w1<2JKe!P}1~=M>&jany zh9eAo5>A2<6 zE(a%VaHdBf=3kCB@N+xe=FNYV{iFBm7gH+FGrmTBwCw0AaN z;awpIT~>#>#0Ugp>ABKt;(l_buuvYMBUAPDe(EI6c)}I(OUDm?r@z}1zddoGBORaL zR)8U$rG}iEI8soi+PuY7&Vpdl8>b5-rFiqo)_G?CrPB^!d{+}*P23$xdtDvIJ+VK< zXmOH8#(sxWef>cBp+G2`rcX`DsAiMdW}3-z{PP&T_@UV-r1}v0yq5> z9zoScmi!K@zJ1>!a_Y2u&^A4=Td$Vst_|)*Dh^Bzbl4*Lfyutk&DGw$t2HG!)e+&Z z_Z`x?1eHwP6bIXU2Wi@jwF9=b(u3y~%9VQClqUXuJFq@F*%{qlN%ZP@DkSP*sbM(zltM;Z*jra&( zXy&W~mx9gKbP$(_8K*Ul}O zn0jY&AS#f~$@3Z9-bbNS+mHGv3#$*ZZXKuqv=# z>tRduJd~rMzJ8*qEfBQ57IV@XoSv!EAwZKDD9^;+z=-&Ml*^Ro@ zE&6ZT^hceZzeh@At$q)a0vxL|LdWYlRzmt`I#BnSWW|aP3jVy{M=Cz&TbKgt3?(9N z)_JSAy?dXmkglx_`BV*beb?L^iT14&G_S+;aTw4@vrrJVuDdg%Y-Xlg7q7RL+}|`SUUD-ZTXr z57Gfr?>BJZwYuB;BQk9HlnzLKSN>NjKI&!2pJj|SfCnP0Zs!m;PCD43CR){`K zsEl!defvJFcGstyWQR)8nJ|YpG!P{2mQfa zUpuNKVmh_SfvS%{P#k7b{e~NEaAwsXeDJ~Kyz|aWPCM8I(nvc?)81`?lWrmWh3El)I2t2fp}3s+sIT|85$@l= zQQ!V7$o~Cye?*>N?WG->3egH(#bK;pOjllJN(ubd3E_ncPjcatgmsN4#BahdipqND zUehJ0*UNJA1+q1EolN9^Mv!`qM&6_MG(EEw4|`{gcVWIllhrpYBJ5K- zHhp!0zIRH2{JYY{E!S_}O%(|7i&aQ6j{`!HVdidRTDd9&G^h`;Badt6wAHfc|vKHbG53?}FAz4u;c zzY8OG+}+UF*yt1-v|*b5vBw^BcYH9iI`!03b(hI-M;lXg+eXwKhaY}8M;mvIa32c>_n>X@;>GUn32c**x3kVVD|gQd>Cc)q zOH)nr9j~D6?6c2y`GBX;|M=sNJ5x09l{-?nPlR}crJU}(bDmQky?VVyG`v}Nsobi& zSe_O9y_&jQm^|^67T2=`h*+aZT^SD7zWnRD04@e2FdWXhDjqWPn`4`qk= zd%5m-d6mli5shfQ<>u;^`Ef~OBiFvZsJmNE(FkfWS(Yi2sZwq)wzk2FdvV*DQqogeeMv~fqX!PZta-+W%6Zy zaI|yExU& z5I7VOc_0q7nRQ|Oa3}*tTEK~Kqb|!Ev|%9?{5ZSX7h3RxHt-S`9HehBaB;B~4!>Qy zNVA{&U0=UiQ-ckHzbri4g`<~x$(%}09KF1`bPHY@?UuIXjSHkuCMaI;!wVdA5eH?0 z;^8+Byfg=o;T!RI_UzfKJoFGQl=d|WzecpZU4riz!JiPVo?ku>iEi{;=IAxrtju@s zen#Psl_I`a?*Zc7zbFrM2!5cX4_Emu(Ys#o`6_d7X&BlnTjpC;4)-XX@!~5A*zdmD zyVuu=)|R=|3HU_$a;X<(jw@g0z71G?q60(uQD)%i+%os=*V-YMf>9Pmn@wF?J1$n{ z20xJ6c`cN=m5Zxr75>L~zUde|n}7*31P4MH;YV+pr#1J15Z^Lz`st^;o<5@idQgu( z`e<_FjW;^WyflEmefy~)6W;3-p6&EvJo57LycjmZrj<6dmK#X&9n9(I9`!wrrH2 zUMXj^7o(ftg=zPrVPED5^@$3}_T#T45)jIj2>6FL-(^`3h5Y7iqCQS4E8ZrX$mZy=;13*!aw8oa%mHpp!k_pSmda>79>RbZxH<5WzNHPF3Mw?Uo*K2@4+5wvkO?+WcK39?v4t-d)$L9;4CQqZ%oU5^XOl zeNP9t%F$Mcuhv*eBcvUAjIYKE&sX@)eHM-CUC+_zK*KgUuU3a3UY&PrngBrD*t~^f`uQwK@4Z7P`=9EQurNK1a zS)ylMPI<5f#qE#f_!{yJdf=P!mAnGyjU)f1%G~NJ;VIMh*2l_}xu@M|BOlOKs4bCq zqrG^UADHdDv&)=vsg|qi6qXUqdIAlEio-l8j!_KL6(|oX7Vv%hxa5gTcUU0JYi9dK zC=<>c@p#Nx1GL{EjCeLJRR~TYGkufRe6_vIp|h{(VoH-6>87ggJe$SOp~6iroz|9lNE^sHvp!a?%pF^Dd?%0T z`ih_BopJ5)}(Ydj(kZT&hd5BW* zQZO)iWyOeRyc+0iO{rZg-;72LAIc`QTCnVAfmd<-;6x$LbIM}NmTR?!@_KFUz1%5~ zOy)(wxu=5@;eHx3jlMW;AF?nU#oI@D*{lxF$JC=9sPz7pMNK+QJ2B}r!{3GR`w*pI zgy>9oXQo{)@3l-@tJ>4t|GJ$)j=AJkz1q7J7+AqcRw`ChYslz zWYe;j2~f85B^%7@F@G~!el-{cpu&q^%sV}sTKWVsZ*{RC+A=*`Thr^GJEsfjs z4;b)9jR-eOaeYteGKIOh9k^rE$bkck18r^9?dAfb5*wMc8`(jJ z03pY2Cc9W_qZEk0SgLbaL63ccdhGKxum4U>RUWDl=ARrXc8a^bbfJVeY=~R8?njye zJy&VH%Z+&ap-O))q`m1_{7lC0;A1Lo$Bt*5cYXa7z1Ql2|ItMbL=JSC0*S0|4+p#e z^HWD`I>4rsKTOEo8s6EcpgrldJg(i6IB?**U5|asmTyU^a9_q!A)%VJpB~mxSohel z;WDlF%uhy+ygExDqXkAFg{QHyfm-l_uS}bb72WHlq%Id<2Wc(m7cOqR`=bRcj6n`6 z4nzggrHk7OW$p#(*3F*_;XaKd>7}Rr0=s3bA)P@Q39NL%IRMD?6|SysqB(D3isq-|GQ zC6pOBSWsUnVLnYGkUwf+`_Edi|A2mv(WqyJ^6fVgv_0R9K1K`IO!0EE@^hr}az_@O zHI=A9+Ron?^FZT(wTc5RUk7-8Od#87`9;caH}%L*%;vjq2j(Av>KE?&_kW{C1KTBZ zzwOdzr&!w;&kJS2FP1YcR#!Jj=5hW>!hT~iX3Sr+fmsuRH*H@~udDNGNkfJ(J+@ct zEgx4}!s@I6lUL)tKBJ2qILJBBp$Y_(CY=AkY{q~A1JuK7y5%%nSTD4|Zg6|2%g+4m zT}^nUaT~tY&~DeP?%vHAi~Uj{oZ7~59;2xOv42T{hhgQ)FR30Kt%H=;2UA%rsCy+6;`Z@_g!LK1 zlZ5jsGhBdv{a&LpN)KlP|Nrfs3zVHjwdcQ%X*w~TkQa#pNji`a2#)}QybNRzmB&>W zURP8`(1m~uYZij9h0LH{U5XCQpm=4WfHO;RSPEQ3c_UncyhL7+AP9MgBtQs(kehTK zeP;iv`hQNHug`ZLea_e2=~S)to%&9__O89F_O7b0YS%1F2VCk~a$(+DWe;|I32Xm0 zlyHx@l_-!mUrLZ#^EP06K2aL73j^vpP{V0n9dhFYYiB11xHySE!i5vZc3%jK!r=ri z?}TxnNTOp>78n^INGV#!XI^zqra3@^Kmhw34v|$6Y555O526)IG%!f>~$3jMQSl##r0o_n6 z+qp-$hqw`zrYm#_4>LkHa1cJ_bRBJVTVZwG-(b9_xa7lD+n%0{j$f27*s7c8n#L_n z*EDY8d6Hl|Y@YGpXI9sb*$4IW?EU}Rc3Uocnr*N-J2KLEj!AjHWxO)a!Hswha890H` zi>|HZd3j&*?XiR#eKZfAQ#V|?FO|2)a@;EOeBk=|P~f>ET24Q?D-b%u&O7hy4EmjQ z)>+O_A4ceoJMK7V)%C8s?sAO{LSu9d3$3A}F(Pyj@T5sQ!X@ma5W+MPd^98(bqnKZ zlqy3eRTQsqaG-Y;U-|W1ad$6Rsm{ zS;-q5?CDwWaDaE!=-OZ0ph-{9UktxVX2JJ(o6-KwX1BK*F7LH)ED2Ay`Y*Tt-l~D{ z#xLi2*xnXfTw*8PPqci0FkWzZ)HHkkPB!O4y7h9Y}9kq=Bm9i1iDqC>(Qw8zV~Yv{tFc(_6a#nJWfwO?p-dXY z%9SgfLK2ZMAVgSu$|5iDu+)lAdY1bz ze~lv^T6yce)Z<-tsLu95O;~HnW2%kB6PpEen&o0laCqelt+7WY+~EP%g%yt}dw2jW zaKUOavv#img9u(;KR&n&yyu-$NB zIm=rL_B{7ihD$u}Ha<-=y4_=n<91i?S6(^Rc;0C|f3KA_t|dI@dIBskKvU>Bd-iNs zE_9{-_3_*fnCL4!@B=>N2vxBO6DF9T+}R0EIs%;xL91mQ zggD<(5Uey-%E$D$8ZJUsf)GW(l9@)oV{9#gPDNTeFpVBTD8d38eE1J+ggXiZ9Dqls zAf9*PF{h0J32`HhaHAX)2x$=x2ueDh;9(LS7~ly$-1z}4-cfEeJef^LfdCsvW1vn5 zLQ8ICZ!|%IAYWtYXBxeym|`8{6v%eldE61;+y^4ufq?>1-B6q;7~&}(<%u9C9si-5 zmhY$wid(am&dx$IbLJ#dIwFo7uV>Dj>1Y5g;0X#ubVCS28{t+z z&zYIo{9xK-K>lX$ry9?hIby_dgYkT^@xYPNl(z-i z2q_2oX(A9Vp$xpVqYi{uiUUU+zRC#mlb7&nu<5|H;#E#qw%!p0C{+|}+JRR#Zhi@C zVlMH*TUEI8~+{bmOBlcZVEkit7)i?Ekkl)Qb%! zGl0c4Vb`KtwRrKXgxf5`<8RhZ|H+if^#*^nDT9*@zsIP8xjFV=TAyXP4bI=Pg%2E@ z`S`OvJDEUdK=G*6<8W*Ddl>x|TX?w>PV&oROUofyJQrO{c$b$@o#*+sN8tuWmGG;E z8*wBD$5R&hNL-nkb@{_ zK9mRUw%cxVBY#HTr=EIha@AE=xp2)AIy(4(Jjxz#2AzuYh`2O92qH&A4T*!YBdr|e zh)9H!U-5*iq4Q3;#LFpOu+#DN;zoYTO5>Ks9k`@L;SyILZp=XZccJ$7mks|88+Cuc zW(Nz6F56joe#t3|Zq;xDXSon}PZx&pJZyP=-DZWS7;J>jjn41sSyhDHDBbF%YdLPT z2@d&jKIB%L;mkE<@b7l_%tJP-_%|D!pJZ+7uP%#bAEAuv>^#kutA1u}b(zg74>#Od zQd?qef;Ll{q}PjEJ#;0_P-~CXdG3`0X~3N}A?MqKkB3GB!}G1DpNp=8#dGl~55Lq3 zawuJmjgG=BMz`t#O3DL)sp%EznaWFeLh#Uqf|c-eJPIRpjl>IfR{nH+y6i@Aqm!i_ zFsK7%h2^JVlTYz&ZJ4;{1)s*LpwwyE5uE4$Cmeh{7C9b!OK2O{C zjfc&D%Idq+XtdU5D+k%q-9Ovx?MVBE^OP+c?(5K`UYIgrcUFjdh&KuA$ITVn5Z$Zs zoYHE=*P?4PeFgX-UHBi;rdiyE5?#Z3QO}`-Tbk}^o)e$9%Tl?zZ2A8S&%KZ#JH;oLrN=G zP?ht0sMX>~(Knvi33jf$h1`ygU2Wv=NBAQ~uu;I5tKYu)732MC@zvtUU5W=Mws^m2 zN@Tjd_t@=)r`e{B$@U@qN^5)XH~(x4XO;r6=g8GOUG545o|Jkz1_UEfz97+dciJ|gv5%DKdAW|Tc z0t2EzGNH$a7Epj?J3gGhZo+b{DG=Mo>m0(AQFLlL;3He`>rU9&^3Un}Ph&6c-@-8oMMk zx=4XYf#FGke1@n2tq_Q8O~nK=CQEq7#?v^ACYgK1Y%<9ahlN?3wzGx;RbfhxUoAe2 zBW*f=Bk}3HwP4p1Pnxvj%nYy^!_5os?(Ul{Z?~Oh@9HhqA11Wo%(7PL=GjY z@rCy*5Dz>xM$!8PULes9XKcv@+&RjY23(&_$+~v}J zDe}D3&xZof#iu;{+!Y9o9A$we-HR@|$aNA7DE{z=KXjH^FTM0qX9*VjSjfg zm=4^?TM0M9sguX;4a3a~={Me3W#-@gSr9M4S=!h{49R#j6(OVSu-X?Rf|PLKo6aia0BNxbZxsYgT)#hv(2# zgD}yqUb^;|=ir5-U8MU;g1`Xo^m5#l*7RXAMDJ7(|Ksj zm|K_0b@EU)qh*8_4|y@Ej$`@keeZjpQx@QcV0z$z2V6P`+|||P?&P3zKy)kyKltE- zZsZUCd+oKCyORUh;D$-_KmYm9McfWN@IYtsop|yuu?sn{{l(}TSo7!4cQY|y0sk+5 z`HM4E51!QJ$Rm$*?r|?LRx$`~u+|%Jo*$57BSJ}JoKF9E%XSKY+I`*3e zdpB3-_Vx{?I1;n=`#!sqW2!3`A%m&-MT-^{>vq^-hq)z1V1WyGV<`Yw-~;Z!;&#I= zTSg_1K6;C#n{V%%Om zT3M51=1k6++0TCV*$+D`MhK5L0*q!QDl1EknoKF0U~tIjQ0N{e?_TEfS(lI7f*66Z+qdQS z2&{+qXW-zGix)Y1K?jUuca_xbj%l8Md0=H#;|#38d;IIS=UxcR`0Z5EwNEk1*B1er z<7Ap%zAkSg%^Vt0s+zuo5r z0{#B6Y>|i8a~=aSK|}ebn95h^%62g0W^nY-|7_McE$(%<0>mwY8J!chH}8zRQv?Ek z8~d-z{&W96Vc1TD-;@5|ga79Y+s0EmG|gAL+qHp`8Yg8;6Ux|)JM;vWz656 zm`Pj|to+qKmK#2%)IY>(QzaYDB$k+5F!9-u_H!1b5FblIFe>Z$^B)pgOM-i%{pogb zKn}z+*h?{r86&?FZzDzySZ*B$ITKPc5g%C-kpQ*Sucl^wlElhD<+*{SHu5G+%S@GT zTAh&(Bl$Ma&5hAUgA&{q(!2L_{Fd(`)J?)*u00@OrkQ`+?~W>xA+%r+yj*l~jq3dWBNcUQQTK`%WFENI%cN$%v{IP;3H%yOZBT z%mE!L8j6J}yG6)vI_N^tPbLspc4uxB6i7MT?-&8S!V}AxQ0g&hn*S?&)cWMi1m*QY+Q;iVKgjzrvYu zJ+$Iij7N#~wwoQe)p;#?Q-X$Z%Z1{fs?994Bcu7xck|+AklhkyYIWiy!BRb)s9}II zQdNn0bO(Ji+juL=VyWnuc&m}SD~=uu^kMvEyIBh zQ3cMS>6>!1*Qp!S1rGBg2)T5R3d5$Ngd6wXs#M-^`6|yAKATcv71JGCJ`c@9?lzcG zwdVqya)GY5LvW>G*DizmFRU{e8TXWjDmE5oiDz zMSC?&gT{)%g7JY51G6vvaKWrw+E9Fy~%iw~x#oAq}ltMa}gn|0IB%dlrtA$m-oQHW_Q zOB71F8{Q#S?JqLEaeFvbAwPqBK{@65t$f#;=o*& zHGG`K(9M#txsm5)+8Qx3ZMrYGliiWV@gSXuwB%cE+UAYSbui`$IS_tU`e#r=w5U!3 zzRG)KxErjzYxa0UCw#^ne0yndAoDxhm*vhZCow_XOvya-x?Qfd9vr=UQrGC5F*l#5 zTzUWg;LguzG-#=f+`*fxJcxbidDFXQi4QsD{WE=MW`{pXmiORN-a|RnPD?v@-M@Mk zy}>*;`h2YuR^5vH%TJMgRYvkP(ZKg!C7a1sk%L@i16qLxY$9Hnr|0rP1)D(oiIV5I zB38=}rZKsj%AjqsYoJ!Qd1b+{1sU4GFGk)2kB}G6r8uCKUGFx;Nv~>tk};j!aQC$G z&MWcLjI0T4*9>=%jUo>Nbr~yRvuQJNwdiJZ2OKKl+&l1BrJZT$f&nXabZ82p;s6#+diGkX1X}nkHKL!^$hztJ&sVM>_G1Y3XB86MO!mWj1@E z`V5S4!R#+7pcA`9G8W-CH1Z)}y=J}qVS}?I0&dtq3WJ?eNYB=WEN*f`l+75oE z;Tp&SX~4Kw?Qbp8MkLbn?3GDh8pW+s)KapN*;)$d$0amzRI43^$$#?9**=ieby9RGyo2jRgQq$bEKKyyq6 zK~)9YA=bhk2o+l!_$kGz!aB%X*TV?IocxEoSW9%Vm(1(@Pv3A@ z6Zdzg3PxCmUQZP)Wwf*xTM$ZZ5M?G4Cb)Oq9JN`NTQ`UU8YjPP%slitZz_8=Y=yl# zU^hO~(!cP+Uxy7-+STBdZxRs*APvF|=~#4aYmt$VJ@6IDcwiiaMu#@F@(KC3 z(JXV?>hSnMdI_4B?7p;m=-@&VZr4z2bgwaC&@w958rlwR2{YHaw&x7>wR)KAcHMO13Mh_E=OO;u=7rCMCA-B2 z&Qgv7qoVOBDHhjPf{ydtSCbZOJ7i-XWGg8g70{0z_v;Q2Sm5>zOXYnwBh~%?G9K zl>CY66&$rN_zRt9gEjTL{u>;OHrReXoKS1*IK|UGCo&xq8VPr-5AFbx5ia{3Ejyhp zjGogVJB_pCo~|W(<tqifCqa)5S7i;iDQy>?N*-KEgG#py90mb(^d7@5pKN0)Cv^FDi_9zgslkEI`&j@5H&8?R7AeF{H z$ovbQRk#G~-IZC88tjl(h4554+biXZZ`35ntOmB@0HAsFhp72M&bZ%5IT%tiE|_mL zGA>3SqxH{Q)*Hp4r7Ce7+F=${;+lBc?iN&~+T@%M!nd4i2C(K#F4owTuO%g;o5UHCMNEw0{z3hmm>@JT>iz$*;D=M(WM_Mswqr1-?b&S_7wDX0j*YK-J7& z5^*_Xrr0`ylbXij-UWQYD=YJ1s6tDEA`&=pARR`Uk0V!*t)ex=H*{yeThCGP+s#hRMO-642Jr#c zr{Yu;*$(!z7|-6b@r9udLw(r&>9{$6cX@;ZdF|jbksleF4#^Ar^&r#-J)EQE$Zk^9N-v0R1 zYXQ$)tu@~{oX~_~V9CFhBE!|Npt_o=3YzO^`gv&A`GC#%Hnt`OH3fB%69e;AxO1^6 zL*KibWtR7|EAKT-M4Y+yuyeFz9Yj9n3A-x=)Q)&Ja^*9~DEOCh6$v#yJXi%zWgfp7 z;$*{A15lDP>m$8sBK(=1v+-@`-n%CI5z2#(#nI^6Qpy43t5Q2odv;NrE_VOh%QWYh zCELc9HJioYpUD|@l@crM022v-(*S^r6t2usXc}Si73)d%_*I@dI(k*z7!Acelggsv zCVr56)3+k7Vfqe>=;2R#zsjPbpSoLq#^}FQwLq(4%16YfWnhiX{Ym$$OHlj7)5xv& zqr6cAZu6EM*EBLNosq$sU2|kG8uPNx&K{pV6ugET{pK5I%MYH_ZB{%Ws;ib8b~{(x zxN1zl(xZOlf25uD3o}t$+}58nA$xL3cc`q_>9Db|>a>1w**dchrc{>?;K>F%gD}2B z(;2$Vz*K4r-uFmA%%_vil?u7XaXQDYig_+Bp{`r#2ng3R3+@Pwlr{>G^!!tfuG04}-3dC6AHSL}528wAIUmgS`Ske1dT^g-pwtw>7MfNtEHJgrL zHWWyFF!yOK-?xnC@7q4~G-M)5ez|UI&n7DN#V;L~g2yA5n=tX(!~Wc4N{wwq-XE;{jCdSd$-PM zmMxsD1zAj|yr|q=7SgvK)plbGHEoP}fi`;F-s@}H^*&LWI2rA=)si%-xycGY$w|u( zy64tksh0)MJ-=mtYU#;;pO(Qh)=;a=7m`uB{`ZEZcO)Tm7yyu;9mI!5-A_~#vh1#m zsVqCH_)v>z&)3w~F^>>@r{m}gCxk~d3bC$qf}47`?ppzK48$|G1A##XnII`i>&QWBo@ezCGj6Tgg-Yl|Mt?ERlT`dLxMZf3X;O5?#Gp` zWuH>vKCQ%?A;PmyYO_{-!1b+BvjH@@(Rcq=yR0#r;{qxt{>!#FKx+g6eKAy`>uLnb zA%CTwdC|6d=L)xFyvYiZU`K%4vcBp_&BR(}Q6KJ;VpUeU!}XE{ET){u`ROK4aWf0Z z3kX3EIX-Y`46trA&IfPGu46Xh*_*z$z@hx<{fA0HTh$^#--GC2>m>RctaYEF+GXF& z^Zt>;w5NE#67rf7cpIVk7zgd>A|A0>Ie!ru1n{p!Kr@QV4 z9f#ysyA?mYpQ;ibb5Ss>1I4tfLRhC#4uAW<6&GflPbO5Ld<)N6#E(kBD`pyotP#px zT1(W+A%C-y4#$Wfu6NYr_cZI}A8yb1N91k}4mC4HNZzm|Ws?pkiSYCQ)-y5mTaeCJ zhm9K`4cvO00HHc01!?l3Jv$p_p>?WhXkfLFt9+ngUE-{03$g`%Ho2L2rq$ts)iLr)ixn({7h<*_s0;8_gbzi#skZ z!lCzY@x8NuA9YQK$J0LOu@wI;PP+ayg5Az1pt_qZf8T75u-%yZBgQ%pSl-elb{EVK zqJeJPG9+do#VelnJ8i9(A#klSTi+Xa45N5!z>*-lybpKT)?y+>!B3Z#9|SXY7e%-7 z^Stk}N1l|?Y7q(>h%D0P-DfUOAxqy5rp}A+2dqE`q^DNs=|g-mg7d*hb-zp)gzwoX z=N5qfHXU(YhrxhVu%a;szQP;K&{<1AK14q~Z(n@*jC&ej3{>}G7Cv@nH=~+8lH_|A zMlHtAn19pe<2YuK*iDZ5ZtRiq)qpiPT&gnm>Gx#`40wh^ZFFgHi8$D5LGpicJjVb*S}L z!J>K=n2=*XHTQ8~wb>&L&&_Rh^!_GDn3Hj{G^Uhss!_E~94ixi%TWW#y=16$nzE0) zYuw!TsLc+X%nnRF^z0R!#KP(C4I2UTm{?h7%Kz-b?bt1KWBWp65;# z&tjt4WLl$*6}pWE&NI;HpY z79=s$x>|qC%t3XxW!B*6L6a;Fm%3WG^?!$SLaL`ElYpN}sej_gFrOC~WKW|kI; zG%Fe~KrX@v7*6n|l_`smyg8V-FBii> z4E!4HtQI#MxpF9s=3{3$ou|98yq_Ee7+#kJ>cM$x1w2hDghgAohyXOpVm1W z8u08cKDC1YN^8=l1vN$(f#gib^!p;B4966sgT3T5HG?U6A5&i&{^#oE)vwQ!oA2L++nl8KtZw9t-b2{Zz7vR>J$5+E=wzoNPDVI+QdelgGi^$p6!`>!;79a z>*A8b1o3+)7Yu(Fce#%iQOI%X`K96>PXbzm5=jK9``T8(mFVd`-=3{{RC5x&x?zz2 z$<`nz27dYowckcwqx;81iAX$XCr3{6BZiRv3PH^&7FOT{rgNC%Rv?8;prA zPkL8nZf~&iF$NV8{4Hd-1Pr8#rRm19Fun?u?Dju=s$MgS-$vjSyDab#GN9#1uKjSv zvRL`nWbwKBOAf-Y@gCr2xAwPc+RM5S%}Nm$(V9J20*9AuT^1eQhS%QmSmH^RrNjJR z1(5O5L|6qyKlX?aE4*V|q+rK3NyxZU?{^Wo26B0eFLWVU(tiW+>mXniiEuqZk)vQ3 zrDo2^+kP&BPN9-&Z<~Cx<9=u3vfwVxIr6+;$f{HF)${N!Bh$ow^I}iIN=0%TLvCeB zqqu#yEWSi?8(&{-{?1;W45!4J%j#Nt@cye`$b%cT(+>UYfRk2|TyGP;*CkUDXRGM# z`&J|ADH#$aKNqN-dQW5*GH@nXtPW)yPM;-m_~^ul-K>(S)M?&q8H05)<1I8|#Ll`> z8W-Mu#=FQ5Ix_tdD|7tUH*|DUeVkp?# zjt#oZ1b*@Sz1OzHk%rZ^K5~96WX7`Yh3wer0HfRfsckN%+7F3+DuT7k3+T=|aEsUO zy!<85n!L8eJc2gNc(9s@st- zZ=MQGz!f8a$+GZ=k**4UPHML5?2Nv1wDQTqA09mjggUmgd6k~Vgo{a%0~MfdnYeD6A(WDlTAdml)>Y6ciXz{bh8jrBE$X$rKnc4t(N6i#m z_Hm@{SGSFhI6GsN+-$wL1QckQyg(5C^UkP|XRmu03AnzgS9e@2Y={(GCA?R2wy+_^ zAZ!IP9|I%Ug^Cq)hv|Ca!F%QMci)Qnf@%B!FZOv%$km`<=NY{OkBtgoi21ok(d8T{ zOs*bs8gJDsu6Ywt&5`RK!SgvAQ4} zvg;YJp3vJR#~VIeZaQakKPfAWKLXGDR$7)xmaLy5La_UqT}AG0SEp@MlZig4AoxPv z8Lu`6RT-aZqLNe-GT9wZZ`vK!qxsBq^!j#NzTi<}H~3gsKpN1re)idG5cQX)&x>2O z;~VRH{5Y0bF*Drx}KDH22yb z^%;5xc<*a;o+i3ApMiK~I6E}vk{LH0J}MreFj!xUpPjFn-k#@}I<5-hj(wK(-pg>jp2 z9|@ieYS+WvaodHe8G62BX+6z3@nnMmnee$v?8BaM*UN*iVe5QtL!X#S);(pe!y)-J z(S}^lE~nf&um9Avii79vGNrwe|PWX{bf{&S!S$C}0}V zVi7V*R$qiWc9fB*BuvRsx7KBgAny~(x#H?#!{a|=BWv+1P3bq#xCi34bGn+f7 zqO`~lxz5NQ+bBs9*)=A2-3`eoZJu0vvf(4$^ad_#=03e8?7Exdu2ndSEE%s$k!iUe zzg=FyR<^coZxA}CJ26{TVbzIBVYgK}7EBi!S#^j{mZimNvL@m)Q)%Y^J@@|F%%L3I zSZJV@jwM@8FQlvdZM<_{H}ZWnqJ)m1x;s3uESxhXvQXfC<#}7f<2NA;vPkt(qr)?QN3RMD!}r+M;_pO2NNY$cI0+MSbCQ|hxn>J zdc%%^lstce>KtxNZU4kK$qGe>-tr_~ltM|#&(j9cn^Ig$AuOpG8EmwXrJ0`^H-xlH7f!XOlLQc`uVch6Bxw( z4a`eTU3V_CHM&f^87qPNRj%q;;Uqi^fDvV7nh@_p162LPxG^==9Wt}aJNW8xQEJ3( z1RlRTt|skAQ6lHY(Y0#vxQMr(pb2Tzv&(*~hYM4gUAJ}v|5%eGzcibw$2F4sfQC3j zoa?#~ZDO(C87F!{hW&kSI%^XGSG3X~!Q+E4Tcw&+!AUz@s~khJA<;xjuugdsvAMm4 zw9B)HDzsU{*Oz*2`JVggwn}CA;ti63&F2s_UDf-0`I4EFrgdh|i*ot(1QA^EO+Tkf zrfEfh*#r>reUJ`)6W3c z?gZK(zVE}4i}M3T!@gBehxPdUV#27Y%Iwh0C+s57Eo1e?qWZt^S~u1qmy6Y!v>lg@ z456AOp)b~-`@A!WdCZdZ$Uv+-h=~SC<+9xrbOV94qCeRrUu>w0PQuVwvy6IhhiqVY zylcXL&?xr5@tPLZQRg-4d&L_iU{m8L87Dq12Ab$}G~l%q z!i#QdpX$fKN^ZdOurVd4+x<5l6C=$O^OY1S@P4y=6&L-fq09>tB`TtGNiy$7JRVK% z`*rB9b*W$`=Q;L!`ciwD zs-aMY8290FKpy$k)jpe(+&^AdX|B;z7lPycgk@566xGzNweq%#5OhkN9vM}%R&61* zTecU+JRGkr8101GcN5n;jrvrTs-tk8nkxo1nV6E*3{FO(X7>u2kuTZ7BM?G$biZ*c zLM2FTs-W>lriMGNGo2BtpGPe;80|zpgKJgGDGzpgK5uGQVzWsgQDS*XXhG&Ji8lys znLnvb3N#Mg49$lrpS<$BkH^A&-xy+ZUnz`X?DS)jtCPCRERU;+;s78L-G`iL1y;A_ z-^t8N@2Q;4N3Ublcf9iH@LWU|`OkU$?8iR&tQ9?w;c{o!2a5%)QE@TYp`K)@dW;+u zsIXWvp-l(2A$EU3>+dXRI5JV>op_n;w?syl`INF5v{0blvpmtg0+hfe`gmI8uIrDF zPG-#m(wsn`qCGq+^F!$a@`t`h-9#{sfv5k7}Jo&Bq-*Q+kAsC8kF^x=!}sxZ{^A%CmvI9#@8cPvgOg2(U&QOkt| zd$U`Tn?kSfU|pHjFWKt;Jk`4m84n^iCo`7D)$I#q?GK1ldf&Ms*F#9JADH)zfc&E3 zJX$ai)V2tg4dlY6>y0*%wvIsF_OU%7dogs@;Jh8mo~N)9hZ}|6@-Ete{M}8g#RZTo zJ}9P^d2(H$An4W6KjX^fnrmKt4KwacwFtS|JW9g#WppP>R(8l`UV9}Jb0WE3>%FVr zL>tLoC8|j0`CnP6zBO%mSZ9I42VL#5%@B(0cicpFufi`Akn`$2gbcpk=`wO=9nOj- zAKLrv@n_T3{;jWGo=&-74R|&^OGHz0K2x!&eV!EQx7LHB>l;W^epq|=g#3pJz-R)T zrU;pW@5{0n;m+5nuU;{ck&W_0BQWs04^EHTy0SMp#Uc3rgu|1ADAE;YC7Bf&8 zva}!WvF*i?k$B{l;&M7xIY}ES&h+v~M;9^=N%{-^d+lF@vtY2T`iRHV2RcdVyn!mM z-rH~Ftn-F^b)^4cof(7n5qfdes}CSLZD zEEZ^UGn`D>BWrB{dZ%Z3dooh29F;q-Sp5T${`0&QfzPHjtSjgbKL;_37iWW62y7P*dlByT5JGgfOkMA1ljO{QW%RZy1}7#Tv>` zCUI>t63U#8!TI5lA%1N5au}cf{C1`jw$Lz)QXkbgnuRugN7Z9Sa?vr#CfrNmjE-_3 zOqJsPOT7ZB14n}=risI(8Ml80*s?6`?=$62e`_)57zWy*8X6}0^?l6~>y9cLp| zG)e!{Z6dBuAPila)FFNR-_CC(gy~xNvqxo|*v?AP>IExWXDPZ-M`r^Wd=*i$z9k=d zS^JU66gQWNdOJN=G6l4~|Lxp{8VVPVq5@YGVLUzf#@c`+E|Q(so*DcCM6Ui-1$qk) zvcyI2si~^x1To|hD5jiL)0@)q_Idxnr~S~42>PE1)7@?GWHAhU*8N5*X=qQE;+m{S z23Mu6PL$Ls5EE5!gzZF+O`k=djguHU@oxB~`=oulM(=6znpb0>aMA3 zKxLP#X1z&;YKbDnK3!gV3>lVOoU#ZAY$0YM=D38|kNe4};(EkpdLSYOg#Tvgsdor|ReIjJ9U&As^PJXo8d-5yHej^_x>fsgmHAeuB0(yBIDiE+qLvK=O!$zp@e7s z?+JAqFlnsCz}xpjFST)Sb-HdQ-#^Tp6R=roVvF+dS5ZwqOkMpqwext7n9*h8EAD+T zJ-!QHs-b=%reGS!!qaYM`v3apfgAObWjK$Vq0DKcV-vT8cumOVf5*^_M!4qtc5}B3 zmaRuJ>9L_H0va032pF;Ie?HVL2>gpoy`R3-z5M^){{Sjn;kS48Hurx5ywsm-t5hpl GNBkc;P;#FD diff --git a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift index fbde8d4c..0513c12f 100644 --- a/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift +++ b/Sources/SwiftUICharts/BarChart/Models/ChartData/StackedBarChartData.swift @@ -156,6 +156,21 @@ public final class StackedBarChartData: CTMultiBarChartDataProtocol { } } } + 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 diff --git a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift index 74069b56..d2a7348a 100644 --- a/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift +++ b/Sources/SwiftUICharts/BarChart/Models/Style/BarChartStyle.swift @@ -20,6 +20,8 @@ public struct BarChartStyle: CTBarChartStyle { public var infoBoxValueColour : Color public var infoBoxDescriptionColour: Color public var infoBoxBackgroundColour : Color + public var infoBoxBorderColour : Color + public var infoBoxBorderStyle : StrokeStyle public var markerType : BarMarkerType @@ -47,6 +49,8 @@ public struct BarChartStyle: CTBarChartStyle { /// - 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. /// @@ -70,6 +74,8 @@ public struct BarChartStyle: CTBarChartStyle { infoBoxValueColour : Color = Color.primary, infoBoxDescriptionColour: Color = Color.primary, infoBoxBackgroundColour : Color = Color.systemsBackground, + infoBoxBorderColour : Color = Color.clear, + infoBoxBorderStyle : StrokeStyle = StrokeStyle(lineWidth: 0), markerType : BarMarkerType = .full, @@ -94,6 +100,8 @@ public struct BarChartStyle: CTBarChartStyle { self.infoBoxValueColour = infoBoxValueColour self.infoBoxDescriptionColour = infoBoxDescriptionColour self.infoBoxBackgroundColour = infoBoxBackgroundColour + self.infoBoxBorderColour = infoBoxBorderColour + self.infoBoxBorderStyle = infoBoxBorderStyle self.markerType = markerType diff --git a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift index a0bb144c..bbdd637f 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Style/LineChartStyle.swift @@ -15,12 +15,14 @@ import SwiftUI */ public struct LineChartStyle: CTLineChartStyle { - public var infoBoxPlacement : InfoBoxPlacement - public var infoBoxValueColour : Color - public var infoBoxDescriptionColour: Color - public var infoBoxBackgroundColour : Color + 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 markerType : LineMarkerType public var xAxisGridStyle : GridStyle public var xAxisLabelPosition : XAxisLabelPosistion @@ -45,6 +47,8 @@ public struct LineChartStyle: CTLineChartStyle { /// - 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. /// @@ -68,6 +72,8 @@ public struct LineChartStyle: CTLineChartStyle { 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()), @@ -92,6 +98,8 @@ public struct LineChartStyle: CTLineChartStyle { self.infoBoxValueColour = infoBoxValueColour self.infoBoxDescriptionColour = infoBoxDescriptionColour self.infoBoxBackgroundColour = infoBoxBackgroundColour + self.infoBoxBorderColour = infoBoxBorderColour + self.infoBoxBorderStyle = infoBoxBorderStyle self.markerType = markerType diff --git a/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift b/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift index 68b93f0d..082f6487 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Style/DoughnutChartStyle.swift @@ -16,6 +16,8 @@ public struct DoughnutChartStyle: CTDoughnutChartStyle { public var infoBoxValueColour : Color public var infoBoxDescriptionColour : Color public var infoBoxBackgroundColour : Color + public var infoBoxBorderColour : Color + public var infoBoxBorderStyle : StrokeStyle public var globalAnimation : Animation @@ -27,12 +29,16 @@ public struct DoughnutChartStyle: CTDoughnutChartStyle { /// - 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 @@ -41,6 +47,9 @@ public struct DoughnutChartStyle: CTDoughnutChartStyle { 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 index 8f5625e5..a22f11b5 100644 --- a/Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift +++ b/Sources/SwiftUICharts/PieChart/Models/Style/PieChartStyle.swift @@ -16,8 +16,10 @@ public struct PieChartStyle: CTPieChartStyle { 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 globalAnimation : Animation /// Model for controlling the overall aesthetic of the chart. /// - Parameters: @@ -25,17 +27,23 @@ public struct PieChartStyle: CTPieChartStyle { /// - 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/Shared/Models/Protocols/SharedProtocols.swift b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift index 6d596dd7..fb647f56 100644 --- a/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift +++ b/Sources/SwiftUICharts/Shared/Models/Protocols/SharedProtocols.swift @@ -265,7 +265,19 @@ public protocol CTChartStyle { */ var infoBoxDescriptionColour: Color { get set } - var infoBoxBackgroundColour : 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. diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/FloatingInfoBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/FloatingInfoBox.swift index 750c8387..a6af4aff 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/FloatingInfoBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/FloatingInfoBox.swift @@ -42,8 +42,7 @@ internal struct FloatingInfoBox: ViewModifier where T: CTChartData { .position(x: chartData.setBoxLocationation(touchLocation: chartData.infoView.touchLocation.x, boxFrame : boxFrame, chartSize : chartData.infoView.chartSize), - y: 35) - .frame(height: 70) + y: boxFrame.midY - 10) .padding(.horizontal, 6) .zIndex(1) } diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index 9a7ecd32..4d5d082c 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -49,6 +49,10 @@ internal struct TouchOverlayBox: View { 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) } From cdd7b8a879b33613e11a0ebf8dab9d3ca845f963 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 14:22:12 +0000 Subject: [PATCH 143/152] Update readme. --- README.md | 465 +++++++----------- Resources/images/BarCharts/BarChart.png | Bin 0 -> 56931 bytes .../images/BarCharts/GroupedBarChart.png | Bin 0 -> 42390 bytes Resources/images/BarCharts/RangeBarChart.png | Bin 0 -> 102546 bytes .../images/BarCharts/StackedBarChart.png | Bin 0 -> 31015 bytes .../images/LineCharts/FilledLineChart.png | Bin 0 -> 120648 bytes Resources/images/LineCharts/LineChart.png | Bin 0 -> 87214 bytes .../images/LineCharts/MultiLineChart.png | Bin 0 -> 149480 bytes .../images/LineCharts/RangedLineChart.png | Bin 0 -> 91338 bytes Resources/images/PieCharts/DoughnutChart.png | Bin 0 -> 103186 bytes Resources/images/PieCharts/PieChart.png | Bin 0 -> 76067 bytes .../Shared/ViewModifiers/TouchOverlay.swift | 1 + .../ViewModifiers/YAxisPOI.swift | 6 +- 13 files changed, 171 insertions(+), 301 deletions(-) create mode 100644 Resources/images/BarCharts/BarChart.png create mode 100644 Resources/images/BarCharts/GroupedBarChart.png create mode 100644 Resources/images/BarCharts/RangeBarChart.png create mode 100644 Resources/images/BarCharts/StackedBarChart.png create mode 100644 Resources/images/LineCharts/FilledLineChart.png create mode 100644 Resources/images/LineCharts/LineChart.png create mode 100644 Resources/images/LineCharts/MultiLineChart.png create mode 100644 Resources/images/LineCharts/RangedLineChart.png create mode 100644 Resources/images/PieCharts/DoughnutChart.png create mode 100644 Resources/images/PieCharts/PieChart.png diff --git a/README.md b/README.md index 653d75a4..c1d85a2d 100644 --- a/README.md +++ b/README.md @@ -1,270 +1,60 @@ # 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. Has accessibility features built in [Demo Project](https://github.com/willdale/SwiftUICharts-Demo) ## Examples -### Line Chart +### Line Charts -![Example of Line Chart](Resources/LineOne.png) +#### Line Chart +![Example of Line Chart](Resources/images/LineCharts/LineChart.png) -#### 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) -``` - -#### Data Model +#### Filled Line Chart +![Example of Line Chart](Resources/images/LineCharts/FilledLineChart.png) -```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 - ) -} - -``` - - -![Example of Line Chart](Resources/LineTwo.png) - -#### View +#### Multi Line Chart +![Example of Line Chart](Resources/images/LineCharts/MultiLineChart.png) +#### Ranged Line Chart +![Example of Line Chart](Resources/images/LineCharts/RangedLineChart.png) -```swift -LineChart() - .touchOverlay(specifier: "%.2f") - .yAxisGrid() - .xAxisLabels() - .yAxisLabels() - .headerBox() - .legends() - .environmentObject(data) -``` -#### Data Model +### Bar Charts -```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) -} -``` - - -### Bar Chart - -![Example of Line Chart](Resources/BarOne.png) - - -#### View +#### Bar Chart +![Example of Line Chart](Resources/images/BarCharts/BarChart.png) -```swift -BarChart() - .touchOverlay() - .averageLine(markerName: "Average", lineColour: Color.primary, strokeStyle: StrokeStyle(lineWidth: 2, dash: [5, 10])) - .yAxisGrid() - .xAxisLabels() - .yAxisLabels() - .headerBox() - .legends() - .environmentObject(data) -``` +#### Range Bar Chart +![Example of Line Chart](Resources/images/BarCharts/RangeBarChart.png) -#### Data Model +#### Grouped Bar Chart +![Example of Line Chart](Resources/images/BarCharts/GroupedBarChart.png) -```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) -} - -``` - - -![Example of Line Chart](Resources/BarTwo.png) - -#### View +#### Stacked Bar Chart +![Example of Line Chart](Resources/images/BarCharts/StackedBarChart.png) -```swift -BarChart() - .touchOverlay() - .averageLine(markerName: "Average", lineColour: Color.primary, strokeStyle: StrokeStyle(lineWidth: 2, dash: [5, 10])) - .yAxisGrid() - .xAxisLabels() - .yAxisLabels() - .headerBox() - .legends() - .environmentObject(data) -``` +### Pie Charts -#### Data Model +#### Pie Chart +![Example of Line Chart](Resources/images/PieCharts/PieChart.png) -```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 - ) -} -``` - - - -## Documentation +#### Doughnut Chart +![Example of Line Chart](Resources/images/PieCharts/DoughnutChart.png) +## Installation -All data and most styling is passed into the view by an Environment Object. See [ChartData](#ChartData). +Swift Package Manager -```swift -.environmentObject(data) +``` +File > Swift Packages > Add Package Dependency... ``` +## Documentation + [View Modifiers](#View-Modifiers) - [Touch Overlay](#Touch-Overlay) - [Point Markers](#Point-Markers) @@ -291,126 +81,205 @@ All data and most styling is passed into the view by an Environment Object. See ## View Modifiers -### Touch Overlay +### All Chart Types + +#### Touch Overlay -Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information. +Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information where specified. -The location of the info box is set in [ChartStyle](#ChartStyle). +The location of the info box is set in [ChartStyle](#ChartStyle) -> infoBoxPlacement. ```swift -.touchOverlay(specifier: String) +.touchOverlay(chartData: CTChartData, specifier: String, unit: TouchUnit) ``` -- specifier: Decimal precision for labels +- chartData: Chart data model. +- specifier: Decimal precision for labels. +- unit: Unit to put before or after the value. Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle) -### Point Markers -Lays out markers over each of the data point. +--- + + +#### Info Box + +Displays the information from [Touch Overlay](#TouchOverlay) if `InfoBoxPlacement` is set to `.infoBox`. + +The location of the info box is set in [ChartStyle](#ChartStyle) -> infoBoxPlacement. ```swift -.pointMarkers() +.infoBox(chartData: CTChartData) ``` -Setup within [ChartData](#ChartData) --> [PointStyle](#PointStyle) +- chartData: Chart data model. + +--- -### Average Line + +#### Floating Info Box + +Displays the information from [Touch Overlay](#TouchOverlay) if `InfoBoxPlacement` is set to `.floating`. + +The location of the info box is set in [ChartStyle](#ChartStyle) -> infoBoxPlacement. + +```swift +.floatingInfoBox(chartData: CTChartData) +``` +- chartData: Chart data model. + + +--- + + +#### Header Box + +Displays the metadata about the chart. See [ChartMetadata](#ChartMetadata). + +Displays the information from [Touch Overlay](#TouchOverlay) if `InfoBoxPlacement` is set to `.header`. + +The location of the info box is set in [ChartStyle](#ChartStyle) -> infoBoxPlacement. + +```swift +.headerBox(chartData: data) +``` + + +--- + + +#### 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)). + +```swift +.legends() +``` +Lays out markers over each of the data point. + + +--- + + +### Line and Bar Charts + +#### Average Line Shows a marker line at the average of all the data points. ```swift -.averageLine(markerName : String = "Average", - lineColour : Color = Color.primary, - strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 2, - lineCap: .round, - lineJoin: .round, - miterLimit: 10, - dash: [CGFloat](), - dashPhase: 0) -``` -- markerName: Title of marker, for the legend -- lineColour: Line Colour -- strokeStyle: Style of Stroke +.averageLine(chartData: CTLineBarChartDataProtocol, + markerName: "Average", + labelPosition: .yAxis(specifier: "%.0f"), + lineColour: .primary, + strokeStyle: StrokeStyle(lineWidth: 3, dash: [5,10])) +``` +- 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. + + +--- -### Y Axis Point Of Interest +#### Y Axis Point Of Interest Configurable Point of interest ```swift -.yAxisPOI(markerName : String = "Average", - lineColour : Color = Color.primary, - strokeStyle : StrokeStyle = StrokeStyle(lineWidth: 2, - lineCap: .round, - lineJoin: .round, - miterLimit: 10, - dash: [CGFloat](), - dashPhase: 0) -``` -- markerName: Title of marker, for the legend -- markerValue : Chosen point. -- lineColour: Line Colour -- strokeStyle: Style of Stroke +.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])) +``` +- 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. + +--- -### X Axis Grid + +#### X Axis Grid Adds vertical lines along the X axis. ```swift -.xAxisGrid() +.xAxisGrid(chartData: CTLineBarChartDataProtocol) ``` -Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle) --> [GridStyle](#GridStyle). +Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle). + +--- -### Y Axis Grid + +#### Y Axis Grid Adds horizontal lines along the Y axis. ```swift -.yAxisGrid() +.yAxisGrid(chartData: CTLineBarChartDataProtocol) ``` -Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle) --> [GridStyle](#GridStyle). +Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle). + + +--- -### X Axis Labels +#### X Axis Labels Labels for the X axis. ```swift -.xAxisLabels() +.xAxisLabels(chartData: CTLineBarChartDataProtocol) ``` -Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle) --> [XAxisLabelSetup](#XAxisLabelSetup) +Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle). + + +--- -### Y Axis Labels + +#### Y Axis Labels Automatically generated labels for the Y axis ```swift -.yAxisLabels(specifier : String = "%.0f") +.yAxisLabels(chartData: CTLineBarChartDataProtocol, specifier: "%.0f") ``` - specifier: Decimal precision specifier. -Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle) --> [YAxisLabelSetup](#YAxisLabelSetup) +Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle). -### Header Box +--- -Displays the metadata about the chart. See [ChartMetadata](#ChartMetadata) -```swift -.headerBox() -``` +### Line Charts +#### Point Markers -### 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)). +Lays out markers over each of the data point. ```swift -.legends() +.pointMarkers(chartData: CTLineChartDataProtocol) ``` -Lays out markers over each of the data point. +Setup within [ChartData](#DataSet) --> [PointStyle](#PointStyle). + + diff --git a/Resources/images/BarCharts/BarChart.png b/Resources/images/BarCharts/BarChart.png new file mode 100644 index 0000000000000000000000000000000000000000..7fe2a08e916e764ee647ef3db384319fe530eed8 GIT binary patch literal 56931 zcmeFa30Tj0`}f`T*GvsVjjcfx4I&Cji6mQDS}X}g6H>PJjZ&s2*%Pv4DMS&nL}?vs zWNEUcv?8QEv@g%=^UGW__y74n&vD=Paoor89QSkoj_bO{q~CY>e9q;4zR&mhxwe^^ z7U2u>Kln*R@9Gx*@q?4mVpl1tEz zkmS0${8{{1W011sSKQX>4KffnR9<9kraaZr#a4No`lyjBMvWS$tUX?R^tiFwTACx2 z$BY^?T4U50jnP^oM~~Lk8lyW}Q~BFx5O;G~xk`85v>D&-#lPtdvUhWH($&!L^z>Bs z)KqtLvC|kme*AcGjWJ_J@*5*vH#oT2c#U*$Rr%`*(`;Q=xU6<^TkYteEUsv?+|k`l zZxBz~@e1pm{(V^o*KftG(m@!=?ZI*Z<88SXyJ_f8XQ3e3td=|9uNrx9J|d#+1%45c;YbvW6+pJja zAYNkFKfYo+&Bo1EZ;;rxk)t$6jvljMG@GWy)-CDyEn~-(t5<2BhId@O-p0;W!^y#Jkn+D*PIs!~I!707$Kz=J^XD_CPBn9J zT(x=~f8jdMaEkKG=~Ksz9zSl}$T8}pB@bzAtUJ@e)y>9Xh3(8~dV^RG_0_9a>S~Q! zF?Q@KEuE3JHrk^`j$NrWYUFaQm9`^oR;+N;|h1N>wkTJ<^SOK zO`S?QJ9Oa(P~#Va>F-onQ!a-?N94gD@!ay*Jg#Ve7!*{#FpEx9Q5tS ztN*80pyRTh_O@K~zcz%9-*9zY<>qPQVryW>Lj8AGnFdd;Asn>hB{cr?`$_)pAI|*O zuM;7}AO9Vi_?LeNH(LiHnhT*TX`xZ1lvGZqnbQmwcpWXz-{2Kma!kK%XqR84jsC-C zw(OPrsl!zhing0icDmX1+QsOtFQ3F6+dVtReeL@6^>Op3m%Th^K6h=ev(aXuOQ$W& zsXrz?v+eP z=NHF$pWXH{*4ldByzbHlOO`CzxUs3&c%GTrrS-3`E6aB8(c_4=Zaz|8YywJ z52_owl`g!nbv#Uty`yOAKVga z8E2n$$>#R`3;9W&8jG*I8}-|cA<}^tp9?J69{W%&+z9{3w&I{zXS!pEPEtwV4NwOlhu6`ust>UP3~HoL|H3>f+mP-@G~J z)TZR{1$JiV1!;A`= zZgtVf^LV{JL|jn6@!dYT6}dx?d%aOeYncA-?enwqo4=M>G^QKw{JAF2HM9S{#Gv9= zdb8twyuyzkUp6qazrJsac>e!+`G6|5Ck_WRn>__?{h zinquP3N{prviZ~^)t(z$YSK;hzCOSGI>_5--1=O%$b&lT(kmAycKZIugxUneHa*5>;5cl%7w-G~oT@@=>&qqNg!&diymc{g0+9rAOp zuTNU_IBdh`_nYeTJux1+!cE+^zC7piOH0SWzx=YmHC#M^ncg^uJTLwxxSw`qk!?*) z&9Gs^LXTcOe|}x&alPi0(0%)Qq#79+MK4XNt2Z$}9Cps7d`6EHU5R?$bw8$s#f+KM`sG<$YjaSn>G+cUwef>O zDl>iSuBI#bwK#=MTE06!K;c%H*4l-qYV(w32Wl-%WOq#16>AYzb9ynPm5#v;e?Y}nXO36WM-^M?u%9%g-SLBL&I~$wIVsB@M z7$5j4WNG4{e3grFac=pE?$4r3t*@ja zzie+#YxjQFH4Bp?82^^unYIPw~g) z5#6sXb$?;*`unktK?uGc*W(WOpWqN(f=S=;IxM#!juFa1BL=d@9(%v~Ob%cgS zm7RX`;X zf9YBF@X3f=Ak2ls=9^-TTxNpr}h0JjM_Mkap5u|LsetO|uaV@*R zx49xNF78~|XQlRr^!P&GeyP6IH)ZNaZY_WM_|c>D=cdQZdaOI+dhM*)v$M}H?vYw> zZEZFW@_jc&olQ+&YPA9b1KHt(FHdI&b?Yx8M&y|@3lFV)9Hv!#d++Vbs}?U_yze9-wN!akox(_g!USx_L|p)ZP$sNp)E;Qu3lBi zW_xoqd~0@8ZF*OvUxG(q(!^ zfrBjFG!lyZ=bP$Xn_|E-J2*HrBz|A{bk_@`_+;$rp+?%oqaW_wz3X27%9;HoiEKOi zYc(6Az5409!*<8VH%!Eiy7nC_YwcK=s&{n3kRe0JO#1WYw;kPG^EMrGFZu4f?}~$S za&oZjwYhPniSA`*<{X~4XwlD|0|)rE)XdyBWK(0wL9xJ(3ilp6mXDpkh_MLz;Kxsc zy7pUmBi2TrXqqCnYWno)#leIpp7rqI!_=$TOBX-=#>PgX<>-knhYV%`4Lnq~5Q zx5CHA$HT(|ZxnmSLK1w2hWlLkzU_^0%&i0C3pSRe29(HI>FvC2QOGTBxPI6YTdy}= zTh>20IoWC4p#ujF?A_aaKpxLT)IO+Ty==*nT>hnwOuXjBC$obWyI-n|nl6W;FGJxk<1k+G`XeSBIn{n|c@y;zpAvFS@`KWzuA1gGK` z7S{cxZ~C=;@Y@o6SD4b~u!!^LM_ifi`N!se;Sf3TXKp2B5xa@{m(HI*e>HQGH-RfY z-{bXn3RZuz<@}_%wZ1U+`V*DiTZ}wEzkiIGww7PbR8MQEy|=4hkWAV0v#uC-3aPF* zn2f;}*7Z`?b;wJoUY4sYdwj$D!)q_zIMb59rE9;o#!SD|hBrHID_P^t8JR8N?Tz7h z+fEx(>BhSt+F?F}c9v#^q zIsKMVWIS2_6z}>1ueZBGk525_dDqXQ2PHqyo;0a#mv%w)rGfD)(szr!9N<%Z6H8ir zb(Q%Q8?hKVPqCv5cr>=-z1_jAGT$EKB$+*)IrP|<47Jz=Iw4xJ{_NzZ+J_qLHNk#X zhdq~@pExz}=>_)nL%5#+Rht}_x<1xgn&i>dT4PyqLT81M`j)>WUMLd5GheuUzNfXz zgTc*hD*AgOBO`I~kNfAFY}@(!loQ)}3~hTiRKN7;>Djlpbd~w>=W3k((Gl$`<$aIB z_5E5JYKqHVJ0w+{i@BKVnDfVPomA4lJ~=dcMeVx>p`}i2GtX+=&;%st>Ucjt+tK&) z5!Xz;_%S_1ikz&aI&$Q$qj960ivB1_@ve#=ab>n=YtvUAyY2e=*Z#`(FVE-Pz`Eb= z(q86t^SfPrwYK+6YkYr%U+^t?mZnr`ztzqA%SY!3S2aS)lS$3x)nE4xo_)?K3urR^ zqQ&_|mkYzxmnOEf)u*+$R{6ERS`lpC)^)Xk{8+~VpIg8G0Cq5%F=IxcC6R*(s|20jhNK_O-L$9-+x_%6h7xaL5y zTMGiRt^GE!R>ftV)@OCisCc?G*~{6$zySY9ZEGsq)lYl3l75>o`3WB7O<&7I46F8Q z8z6o2-GhC8Ut_HM7ZFN0B+5Z`_&va7bFpYN+b7mbBS;H?MM8}{MttW%V)YPj9Bz1IDUslYJ>0v z-0R$$ZOXErUS1Fp39zKBp`ps!f3 zMs7e$Qemt5B;WRdpTG#hld9c8!vz1BxWZ1leFu>@%4XQ+6PLn6dN}Ubu|vP<;QN9b$4+$ETtC_uqf7&NovX%O7nY z9thMwuz&x$oGXKUT2s!hZ46`k9P$&t7TslCLc%iM-K8dJFX$^4T<2t6Wj4QQrw;1pWw@)a`QQt_v8?p21J9p7CxVrer;RDJ z9tLYzr?vq9+OmRupPw_25oSSLK9}pxGBnp!OmKO3-!ZlEjCS3Kh6xEA8!?iSAqnZlFa(dsE* zo>t!l5d&UgoZfdjd&WqcgyeG>-ml~jqW zBjOD~`%aG2|S{%yhh$k!*Q@2I|u6L;y- zMXkMvO&w4@0Lx_e#4 zZbfHHd<>J_68qutk@}Q<)7DV__S0J9R{bXT+FHZqg;lA|mEJ?cCwNxYzZ4JmM~Eg% zUES{zengxrTu|{YBqh{9eprMuhW!vYrMS=xNV06%GP4`cwdU;$4K;5Y>ZSbnh<0XXX6&PP*dM^5q2LFJ*T@bV zF`|WtsFW&_o$17yI|11rN=jPEAU|4Ld*-_rdVjHhH!*0*wKcY@R<&JPrFJ@OQ2wM% zP17!lpfzhtXl!zAhOgb$9{|ey;Gl}kNYg{3ib+W=1yOAv!8zfR^kn^cLxS)im*`st z)2_P$Y=(EgFG4xB!Vmtx0&2lmGUH87L`U0$TtlK#J#Q$>3h3!{D2ad;V4C{tO@F5; zk^+Bjjeh%QeeBIZ{?6UI=I?vv6GGbw?Q%g&h!bF2JGo^_Yw~4 zz>5&HCAJV?F5Jdj>9D4q$uHdmFEMz2(Yfw9D*bbYSS_15^T8=H-+dKPPpV)Jlm7e# zXcl#p*YG zQWoKOmdSt(yOf1pEJ!01Pi$*=TU;g>4a498h0R6;G*jKu$m*|Uv1z{5^H++!|Dnf_ z-(y8CS9-b5)_i_sKk1vMzP0pi%tjk?H)a0nwo;Oe^a-epK=i*?M89SpN{U&9u z>f%;KhE~~TH*NXyH(aspk4|zEtN-W(3|;H&T;n@*1F36JcCUh?9?J1~F0+3zPQ{o8 zIam0OTy*Kz>Y|$=bq)lz`@Gu5V*s7vG7?S`ar;%(kA!|da@?)#{<>~DAC9^Xf3~tP zbrT7!A!T1X3G?in;gan!yblsfA_Nx6n`ZT=U}`uS5m9vJh(j)m;`A{ka#(x?xo;y>T zhnri5+Yp+&TED|uKUt&=A2#f)nNz-dcA!RYbt|u|=?W$9-!D}4A}I)hr`kQrvY=zlIySuip#;tt-o}BF9)Ix~&H7H(trh%M%bfa-Yfem-A@T`lp;!8lV077mG}XA%+;y7kvxu*yUjM)VQ_us5*sb~X-Qit!9qN^bfT zqjHyMp!;^G=Agy4cPJ~y{QAdcr|Z3?Zvu0z+w^nvPrjLQ!tCq$p3(+3HpPqWj%ktQ zYXijo0K3D+uD*f?@Sn%w`e{BsImACKA`vXn>r5_{5`d?n3EY>0$;rii?B|&L(>80* ziri4@ksAUdIAyTvrlK^~Xnzhe$z=WRAAAO-Mx>quYCYyzusY|8t-Q2%UHN#bd(c)d zb)#9c`i}Z_GOq;QzUo!;2G3Jp5~u1DGxyXCC7)`8cX`{YuGwWC$E*@=<@B66bEY62 zu_Xh8o5>qK5C7%K=Io;`aO6MMJay{R-xuHU$FL3jA&@qRi|yOPUcB{mS`>mdUnyn1vF+`fG~oP=Nb zgv>s6M{5ApT8;(DwX=r~9qI=jg-{YjUEjt7{2uvDA!nbZ%{IXs6;7n={7Gd7sJA6| zZEV-}GJlrd^zfL|Q;g@$drDOL@>Z!mp|<}PJuFr*8ReJLZz*-@+ zwy&{r?yJOdHT-tnA-@TJB`8*dz%@0)H9*-Olp}EY3v&x%it@xO7zH$Lx#jrfceX;T zoXf4vi_n!@COg~2M34tn;+wdQdYaAkWYSF60TUZ;%jx@+U5G!q_rsQy>RVkLlWS&t zcydh6P0@g-d}gZO`e11IQC<_OW~85*o?h$ut4}uas3M2)5V!a4+b6P9>9Z)g0TUC* z>&+Lo?btD)WdD4@>=IrD!#sKZQn#$>J9>{yY;S9ZXAc<0-J#<#yJ+`0ZOT8*(t%xgiP(V zMpVxDZOMlZb}LuHi@ljVx`fh~--xHgBonh9F7EvNw+&fJHkW39Vm%wraYjy-(vV7yObEt@kI)`bi6H0SrO zmb}_0c)%fZ4voUG`Dyv9%PMMWF)WtdQRjOwV@jdEC;N2t!JLfFH5UnnRZ6Pc5hDfh zPvuox-NaR-mT&oA`h#C)BuQ?wx&11sKv|YmW9Ngv%)cx;68%fHI(O-!?U47gv;nw@ zV#7P~prGQVcJCq5lZzLmros`!p^uLoOub}2Z(gk+z69y?)b#GjPp?i-fE!@1bY%UB zT}z$s_Ihz?khp@=FL{8**p#{@C1s*(kw#T#c>dUmtjk^pDnW&okgGs&lBaHNiui(* z)_p;^Ie9JWn%+k+v2u&N(qjvR^OWS6@jYK`EuW5mQ4S^;k%^C-iooHExm3 zA}xC4*pf=3f$jJO{B?ae6o^c2ofYf!e7@;YsDhD8;*3m8L}4>sVdUqrhULUwXD6Z1nYh~`)OXI72zjko)To%5d(LnyuVa*TN z`Nvn6SEOwGdNHXoF`#{c6#``B3LJ04*VChD{}R+~Nl^QxO@*JPbClrb0uSW#;IXvhNPf&hO1p$FVv zn;k5(9Yh;JE91i5| z$CF2o9wmiI8#tAOT5Or04b%##ccrnUU-xQ-f&9+CTAygxxd$fAJ}~0&1W$*A5o5f7 zg`$0DAn#UH(0X!*y1-PuHI_}py9JN^{?TEwV?`k~L_%n?NP;s-Kowe5Iei3bm z@JX9!%`7~>s3r63asAwTfylM_>e4upn=rhSFF)tqSFC@mqkZq{zt@JcJ&s)w`zfQ|Ov&wZ>e zFQ1;!_M6Or=oafN!cI|peo(&}7>w@?_UzdszqkZOKvqLgr@edi3e^xDk=yZ=)X$qN zdDTW(;)lTp_wPUIkqb8$2PSOs@;XR+jTI6+aY?(p@Z;zuGC8Ncqt9A=^s9gUy@44i z?(F=i18L@FW`DvLR_BXyr!Xp`>gmW~!%FcTorGrSjE4%bmhl&PYgTN{b9*YU636Kt z>lYdgq3Z$P(ofm|NNi(c(^xa!_5JVhl?B9?=2qWx3ymkvj;DzLrF=x+ug#jW)=7TA zXbhZgsj@7ETFm{T1n0N66eu4Y0?#fZx>26ts@`c6;^b!POrbug#N>zPb*{Rn*WU73 zrC$(rRjIP(ILXrDehnnxnEBDJviB zg+8o&{R$YAPvxQ1kS%iHA0bY!&eCXaZ!K)!lrb!7ztYUvv%T9}>%rhXq;HDmP4(%a zBy8+7kL_Qnbe!!{Q2CndXzpWn&Qkr?{_nG1C0;q)^ z{S*|vEyRJSLsO3S?}yGd_OArPCw-IFg^CKS`I^Xhhk(Y+EcvbI!d&!l_iFmeIrSj7J*!}zW6Lza6hwmvbr$V)(Q%xw0C;)mnT(i$Q3F=v&8+Z2I0{v-SX|X2@ zvNShtHN^`|F#p>O)f%8I8#pDzpWqJ^TaMWPlb^!f5TKewgP*dl#PnQiao=(4bYM&c z!z`-2#Ja|wv+xpwzs4k23S7tH!UB0m;3S({zk`dumOit)zh|I@mGfP4unkiL=hr z6#CSIUS9xe_DuC6I4|2!5*je)=d(Y)rm7RW!%9Oyu3}k3SD(Fgv!~?so+-}w)Jxg` z{MyY;tv2;a>-X1w3aPG8zDxrdQb>S@s^9+AD-U0!wd)I@_9TcC%u;Cp&>3V!l0MOP zAk9NE`BeRAesai+L|86K-bO6GvXaD(-RI)I z_#hEv)EbiZsTlycPjKI*uP(opb6iIc3a`UzK8y7biW6CXn2N{8^(HhF?(?`!p*1KQ z%}cK5ads0>O&B_J`tb+wkBi|{{He8R8MM>g zRYHxTG+gt6+=3n|`GSS&{NaX>0qE7|J)6Xz{zjs|5mS80l3Q z6CM4LHe12v6^3MYC2B@in#?)Y#goE;u4%(>j|J|Ytkpd5Yf9V{2Zu^f`I5mY zfu3)-WG<6!(|(#l>dR2t{3owpJ-s1tW9Vq^N+6InsHP+qMNx|ek%g69Xiz4x*%V)+ zPwsn+v|ZG?->I-@sH`n{sOFb@d*EvNRJ|;$Y7VBg*kUX=6QTTTte;-@F6L%$2MNee zO)6vH1gx#{ZmYqbgviRqsj47_zH^A@?&&a^f%Q+nmoc*VTwB}iS_(PDqx9)uS%1-Q zhHD}BclIwzEW4;?{fr1Kh!|Oazt%GA=KSWR^XBb6B0l|cn)No+zN?-NZmGLaShX0X zIg#Xc>z?Oos2=de*Pau#wLK|r4djn{z1b$yURA3Bwo;aza^g>1q0UkWy{M8p1^(Xa z9v$EvSorqsb(^ZPss8*R)np7Vfm@(d21K47K9_g9bLWoJDqRSu&V^4zOS)!S2)6nNC#9SRl+BBE4Jhn`+}37B6@Y1Qy%l>L@I-M?19-&WJmMiogA2V?IgS~^(`+Hh}%|(HAKq*-9z1hHt z0*->osn@suq*Aw(bV2(d9($Il`Vu5jT^$kWV-h4X|iO3+ecA|_4oOTyhRkavi_LLQdT2aukIC% z&!vkN>9Ea0LqqpC2Yv+Z3kP1>c9sYms3Dr$f*|6;1oUqHMS^Ihl9HDetQ%#uHbE1# ztt`9nqSbhi1G0+m44huCj}O(4_-gb+{|%_nY;1K!L-^KLs6g?QD<_VwY4;1Vq?DlR zeCtS@;A-x-07U3vSiF0vrne8EsE%{l3Bs^lAJLkqC`8>CAd^|GrM-6M?=WGKeO; zoO_{VP}s)6DJPsBQ&c?@lv`TP5)Y3$wsrW3o*T~Hl2BU_htR8)h}J=16a3r3c8xab zu-i&)@~K}RYZp$dvj;9C^&<=4PlvEs0g}}J=zacZU(f0(^3o6%jWmcv0<0eLq1ri) z1*lc9_8Cq4d{Y^k#N*INynh^tk`RQh6H4KPSu~6iRl_rSWu_A<;yMS5l*&_v!&e*x zoF6aVqr0>GQOd`YJDxs!R&V(XFBD=s$n|s81u4`ily(yYf!knhHZ6}XD$YKhqVkr0 zDuGSc{gYrX_}N)nd;_n^h>)XoM{Z_{LqhQuT7dN*vsA|H-M`-rW)CUZ$d>T?7WYI3 zgHIPEZ;9p~Fx0z|SYi3F{3d}-^mT+>ke4+zHtrfcZNNm@%T#NcP>CdaX~CUbBAfF) ztnI8&n84pek?>}De4TnHux|Y0_!)~yd30|PqGl6@GM*m{4ZX1F@^ZlZ(nPmyKBY2q z=gmWJa{zPZ9Tudt?md~*p2l(3&sUYp0i;f8b);~Z-!yJ(W{ z2?=qAm`G2zD7%*vHlXz7oP#I~h5n4c*MLn;&Xf>r5q+QE2~b#*AX1|Qy%$-eHHA); z{9Uw(N9n?sm_rTKl<83&XZ(!9w=!=7kbRJZHj2<4?CVG5m%a(lASlFYcUU9+hN4az zBpP?K$K_8ioTmy+ff)rD!Wpb3L!~7AC|)tzx8o|C4;6p47eA01JvCAiKIAp0CH^n? zLHASnlONApvLtc&-R&@D==+jkHMeb(Y4+`QmeDDAu%HQHo~3o<)>4B_EMQ4l`p!Ng zJ+f@=9}V?CQLG^25_*|V#=b@peUX&;8~cJqiPRbX{{F26!`0QnRnVQ=b|&-IfLB{c zG4iJgJyEJ)As`bQ0T*70A9M~@nm1b9db0VpE`d|BDo-R*cEqiDu?DMvdFW`15ID&m z>LW%7nMhSydz({^XzV^58Gk1|-4751WZXXS-w*WBev()5WMRofR6NzVSn&0D_mxYR zF7e7V08AqZ;x;_=NHsNYu<%3uzrTIx_30xA4`ySikb0C013dGbi*EuTo`h+Y(WcFT&}sK~8CEf{mxQNXaAAe&(|ocv+R<gS^rKc&qmN4 zp$1e5TF^3eT|si=rEt16m)$R{U0SvzJMrbUi~sE(^X)G*Pwm~bEPYla@0E)`ITwyZ z1VcU=DC-Y@UQvIu+AHC`*^Ylj+*`6Np)#wNhHt!UPKec8*(Xaivn!UwR>Z!}^YEF~ ztK}hHx4F|dJJJ17X;KS(n_!qkpza(`504^-V0|n+TqwMY9pLXMunz;|OkT=i;%m-y zoqi-XyuNJx;0Y)5@6S~odAZFxNiQt)UAsVbyZ5hnGa?@?tY9KQ0-{lr>PaHNZ$VV> z{=@UaH0=Z|>o?Y3ew)4Inh|F58@(F4)aI$Iw0y8T=wOHHM+z#M=B2%w=#a)y5hm9ULMr^&{ecIkT^<2AN=C>UPlpNFolm>HKO zo@xw}{U?@_yFw;gXU3TI}PU&R3grGsCU)RCW&xJnell|JLPO7WCW| z2mTe22R@ZB`&;|`pC4d|^+2{AQ`;P@dk8j*e z)XH)=dG9AlL-CJ+xpAX&`zvbtylCziwfkdWMo!O)ZoB)We%jR0SS*q+S-;IgCjCe2 zuai0;htt-KS*P-Ej=pV?63jHyA8qUZZz10Niu#MUi<*}%{yW+axKK7vZ}}Tf_i27c z9^c0Lb|#pq_N*8nwRuhL2luM9gvwUomkA@AuUKBy*x>=jQXCz-^T%Zy2J%w<90m-? z^YM;561}+m|EI2(FNlFNCdU#%y!P6Hnw2t}yF9L+jz9QTOmB6{--N0`Og`1BSksWA z!mFobr2zn|2_hF#$amcV zq^9AM01ROhH`EF}75AsJp^6%y`ivwBMo%$xjXXjim%hoYRydl!01F3b1jtb8LZPqf zs+}i40QpZTgJ^n?KB8uYP?)q+(gc?}_cF`pfOrn*)r^d$f(-gF$*^>=joaVBGSPgk z3^5!)p#aYnq%G+&PJW>qrjlRnkxQbgx^PtA&zGqg1_#V+TBwHkRmzvsr%wls{>N{3 ze!Uvasoa7_*i>J}I4G}|2G%%jd<`R2oBn>G?-H>pfUU8Hi6IqnC>P72pTUk{5Tc^D z1=tBaS`6h&gU7GE&3uC7;(rv6Z&JxmAK<<2&eor(lPPy{C_T{mj0VM254<6x&5!^( z-m8fAnO-vMC7I)qa(cD3f)i0aVzG5m<^+Pgxe-)P@Kd`e?t~jq2uYAI()f?1@_qf~*G52-GYJ^+^njxzay&WSo!m%#*c! zJ{Kl2A5jgcf%qSE>Z3>3mc*pR=hx_yyF>Kfk-Vwd9*WF$gEokPp1=DmanR=|l>^t|r zCZj=RV2w&2xwX^nu4UP=!8?Vnml9aczX&2?_m5kqoH*i;xEkgO_N56H?C_pFW`mze zJa55;Qy!vUCJ-PdGSu|A7+?}gdrAhOd;|-^J&B1TB=FOR`_TwWtGU01&tszCimu*H^8Q0(ww3ExJ6< zb5J=-9n(Nk=4$j%rzZtT#?Bx_DU3urj%s*qLmbeqWkY_m#zk-a+@*>=93G~Yzbt=v zQqbatkJMBGES7Ec=#jqVNRW(n;ed)EYYv>+aL{2I1P{GSC;sQo9m&X%l#I*s)B#dKEh_KMm7X()dTrtpwt6k|(ge>Y& zV&qA80KovTx*Qj#(VYyB1qC7q^$8Ou3?3}xjD$eyq#Pa!mQaCa_^L;T2zAF6kHNx}}y~M-BOaiuRcMRk^?cBO5@74Tm zh7xY5qWMn+l2J{{4Jbuu(Eg>9B=zo^t-KE;Kau-wasMr>=tlY+ls4uYEW|!YX)PS9 zlCRXZO0#N)q!`^jjHi64He!UJks+oamiNN*5FC|d;me?L_J@Rouq$jRRq6C3V{w(u z_S=0UEv>AQ8OVe2MwOwaSn9x70Mm!E{$IX)p{+k-S6{T%bsw|dLM*VvmIw>tEG5ux z>uy!UL^kgjIZjN~eF*Gtx7+H}^22SG_U=cvwQ@-TIn4)VIbv~W`+XrC4DcxE!8f{fRPi8+D} zJpT?zUZ}Zgr)v*{C;uXPq;~0U1na56OE1o`ch9#?bb@>#7tV4&|;l^*%gRPkb#A`iKro1HdZ~wMc zv)L+TSyGTbCP9-1dPk_Tkk~l$pjhthkV>KmX$rKZz3!D6tnGR2xp zWpytx(jGnfGAN8RPZm7Ne>=Q@Zo*!g8fL2d(b$6L@HsI_N2Jq7L`vtQJw;=l#B6ky zOxGW&GZvGocym%$LhrZ)oR?>L{5N||IMBESs%@Rsv3+xUJ~oQKbV7Z6i@zrP*TC z6Bu%1qpXPuLW}X*F|9ueZp1KJo*l%ld(B*{B#E&NRZ%%bvsEU;Rppi~a}m6s<@I%E zk9X^cs{4tmH0Ak_U$E5Bvl`4Wsi}3*caM~ugCG_C8_Ll$QBihiL_U0gjdW+lPMCdE zzT0oyxk9 zN^8olt!@uz&ziO7<@v+UdP#m(XMR=El_n0Cl#~$m7$U8{*@a9_(`38HTFEbES*Q%k zXJ8CjG+Nd%r5B%`r%v3^H)wmE)@r$G)qCHEqy*1TXQQqoRs&TDr5*yG324KJWos%Wz+U;&n^OrM%zu(Z za4JL!-S&T&@%uw&@p(THSN_f^JKZoqKIpF*u2Y+Djk0s$!rs20jOrzbz^v1B&8Nnu zrkVUgW@b1YD^`rAy}@>3_bA(p{RAysOSnaDnCOX1MUOwZ;eZ2vhALN&w?v&;MQ|?q z<2Mo-r=d6_-Wvq9KX}824WLGGT!QGH5Xn(IMIvIk+_8}M+gL~ig>0m*)E}ka=5axd z&TqC2#m4`0VnSblJ1|_gl<0(3;HYe(Nicw<+IT9tYns}NC^)+BB5YXF{i z@7{rT!MD}v6w*kJL{nRIjmcRjUu%l@njAN6ys&<NlJ#uJD>X z#CqVa$C#25KKJNm7lApxe(A7|XRL{sJQsGzt;mLAs2@G^P6vI-lWHyYaXZM&+hZ)8 z@YF1#7Vi$0%BqmGY$Jy3aFOgYBgLY&XUqqm5E?NW=qF!y7gw3Qt5->Yg1<7_@FyG{ zz=~@lH^B+ZbFamaRkIu<=V(O?0sOMSXd%Ram*^nznKUV*Ur@)Ao@Yr3p@$Eb=67@w zW$b2;klf&Z;FPVMeS4Nt6Pu~JRfiGn`Ftwvc#|_{4MK$MQk2igcod_5r63@`H4-ag zu1dCWbC}BL+^#L*s8iCuzO=*?63eeVL~|{UG`N3(nt{&1PSmqBs?+FDn&mqBvkv@FgRAhlD@z7FPa{~r}2Z=B|u`@N3_L}D( z#c@VLu--Wku;dOsQV91zO}Xn^>d5M;RPp_A(wZ2wg;cOYXiKdqAk#VXwKZ?<;*|iG ztsb5o_gE0%A+`jA@S&&@VlMQd6Wl+!<=7t7vj6i}jtdat6*_-H(2Y1%tk$+|TG1Vm zX3hM7D_PV93>x_%3|he0Bjtk_G$r8=9y&A}#S*J0RAeG7Ku(FC8Z$uKzrn@1X1t_m z+VXyMafK3*BMHDMjOEY}6A~uoD>!ka20Eh=!Xwe6Vt5C2u-ll$UH?2fi-Bv=@+aB> zNDiija%jdWrPgSmDz{z-f{oLagx9SXu|E$)>lC#l+^xZdnX_NNdHYtBmei`B93{yt zVhd?V)bC8Gpo>+a4_BQMygzmr=xErmnMOv{*T;&rEBg7T$D`Z;s6v%`im*`z z8jDI=tUZRQ`9$XwY18oHOz|CTI81`!#Zgj7FzSV%h5{OO3mHb#?7nKbsYphNY|vlh z5a*DONqJSG;^HPTH~xUXm0-Pwtcm+VJU{9Escp_Adrur#_{ZOkIj*ntYns*6S%(ooe zxpQX$%Q--W*avPfFMcEzNUC{z?T)*+;0x}{YKjohMM2#v*{dcaR88m!I;Lm{t0N~( zN`;vLVytCh7(f&Z3$y~)1GV2pnXzgc`Mp_0J?swb(4$8Wq>~d7{ss-$7=3H@3u3N> z_BO61lL{)Y#E^v-L#T*3&PGQ5BI{3R7dkTRkTkA)-cbT`i<+mC&t`X=tOce=awkT) zB(+fzDUl*mvJ}i&GqeP&%wYU)Cb{`yq9+!87DO)x0bL|#5`(K2hrm_8q`1piAs^cxh=mksWO}&~)o4C4&pbO_+&10>F7#5K%k^vj~ z@2Iu)Atwtbd=^K5@MwszmtzL(pq&4&Y_u4_rHhIr^CG7NIGR6|q^-%{h3Y0L&qzVC zW*{$G{2@Xa!PXS=k_>(H(LCc*BO&UeA6@?T}Qz0f#w%2xEK0_9}y4y5tEN8t4KW+InaA_iqbO3d3)!Sm@1kpB6 zt+E3h7xKsJh|rZ@>WH=dqpknrB)9r9tQ#z3NTJq(()e79*UE#H_d`^Op9^qlW)Q?& zzEHac_PG_}KuKD6Y~sX^9W!`Evg1+0%t&TTT=ZISP{ec)0~ulp86ZyV8e!dIx0_Ju zQeqQ~#Y70s-~*@?=h8s8>@TeDXR?k(yiql!qmNv)Rd>jse5Ca>$JZ8@Vc{OgAegwG zV%$&^2+cu;-1ujN0VC4Ad$(0M`X!-M7H6e^fgz1mtZv{uMN(0fTV(KU^aR(dF8>&` z5?mP6x(?FnFA>Itz3yiRq~}&;{H@2Rw_kej(@#CpVpN)@>JI6)Ol&DWE|{UwEAIWs zm@U1V3c#PGV~Tm|8DZ=jzl!2fClhpu%nta|VGNwzv@-GvrJR906?bKlnOBS>$_BYd zt{vO=jjR4tM=QaVmm%ydjkBMt6Rx+F6dDy3Meg8e1eQ!$)?XlC8UfxQf5|kusxRUc zaY!4!Ddsbb+~uV7{|L`cX@8SgkN9A8@%8eD4v$kuwjy^q7cL|HFpD};h8l?I6OI|LI+QV{NdwUz7;(oL3}()C4qj#+>7{bvP5Cf=Mmpx)ePtR{C%e)^nt)st%;FO6huXke6na z=m{=U6-$S}c7wdMQ2wkO1yh3Vilz+l1`ydr4zMo{DVQ!VOPdjj$v) ze03}uMRZuz>D3#q3H2-yJRiW{!$juLI^l-~2k~d((&xQpoy?x)-Kpb@Z7*X z7{5(`B~FO@1+3yyMpC2o+AM_U%$IOa*rs!1K9faT#33rgZ+eD${1pj~DG4UwgfIu? z?1TIFRjrTy=T1_4yLr*yt)viIS*Zz77h)1xoOvNiatd`>IdN74_T-p#@@}z@MCd3L z3J)CbFGg)L_75Yll~epRio5{JNu%yK16E=l4iF|#`<*+p+>h_y|ApsA8vKk+WR}a9 zjH6=srZT=k>^6ENn1FcrOujBkr^$B#J)%bjY^npvhhjyao)4>SlVG*YDv`i=;yBaR zt;p6F7O!7r!^v@ETBu2%rw$yNI?z~H9;b$wpBXc7AYxe)9sXzdd?Ee}=#Y%kB@2r+ zj8X2{8R0N9zZ_^$;T<0rH4eC8{R1Ph=SNMp)l0$~P|L{SKtcn$(zLTtr z$%G5AG+7QE#6{;dW|8otLWNDmKpw9WoqF;<^NP1ZDluFjg!jEZc0vvuIjYZ)GE;$-#dXj-K*3HppokK|NV(Llq$#Rh3@rYIM6r>n2%-lJ! zTj^P>0qCUiyrXh8?oIRHrvG;k=9v#3jE{^G#?B7@H-FD;fT%X`bx$Kx2!m~sarVtC z=EJ4KLyBYDw6*K^90%)M-EH=*u(sMS`OE2-C2PO=Rd;G8iC0CRx37Gvx0S&lU?wyc zD+x9|9Ti0$h1ohL-_|QQxPMJ9#Dn)f7ut+1iQ}0HS|_eEnNFHg`U(9jl6u z*a;bf5Zy0WK{$PbRUNhz!QKEm`{HK+tu5X9Z@k@A8@m|j zGncNHXh)0jSQN>h*wk-{W87WCq~F@F0zs$_MCF!ks7>&bQS`miXjFp6&<92hy2O z^GJ@AgLj?{2Ob=2(9sK7aHQ}4T>@}X8VdZ+$$hFl&pC;1E3HXYH8s)T2HKu{I#677 zvfGP@>{ARRB@#?U5lbLjNG}YRBX81(g{MIXx(P?*%~x|3W+UYPS>l5P#9>l2$4PEi zX@PPr23=HXq(nsgO5pIs;CRY{H(`jdwbXU~~4 z$JiKFeTRpHQ^$Qimbr^<)uk=j7?-BayJ9Sf?&kxvZ&iUj!1S;!bl;(T~74a@5zXta|(2~MCC7Gbuj zX(4BL(H8%UCHKc4@mgl9Pg55T9m-G@egfm9$Xq!s?;Yh2==?zGQ$NhDkjzUe>xz&} zr}@AEk$4Fd3cY$g=2Xz#njI-Y5nlkvk2i zz=5J?Ohmv;N^nq$pedOw(7{Tvh<|k9B(w_Le>aR;ghYgmx_yS1^|hY;EM1+KGi$2*cUOk#orG=<*mxaaPMBMd3GjMU;0H9UC(+mfW)2mstrk?8^)W=`5S(w# zI{fZu!SlU`s{inTvy;mxT!;pFWC8*nVU&J@I3oGL0jlYE8f0)5y?Xba@%LkgIN%r{ zEG(3EnY=W|5O9FNT?mCj%I*8d^UPE^FMffQmEwzk@TT;i`5HA1=iO}LjBN3&>Py8` z4)@_W6{dLzv!Doi7r525zwc*PdYuf+TViMoSwwMKw4lsrsBt^Ip3@;x=AW6!1nEE%6EQ>Oeg&5N+Ie?MZfZwa)-k+tANg-KTO(gw?y zcU>b9g+%1ukz{9bBF8~83Y^LTifQ6hK05ez-R6nMmvVB8>EO}DaUw`Q%C#>Ol`s@h z{dQZ>@jK6VwEgfsB@MK}Fyue9E!Y}1gtZb~^I9!J7>veI-g9^V2`qzRlw@5MkDOF0 zH90({1cJQ~lf~@W2{HGJrl9kQ3cLZ?k`Q;&_uzUv{(@rk=Lt<5z3EIH_f5$JYbQGY z3JMk{(AtS98crCQ2--jOv?GH(pRWH@D31iiXk|5#uMy}HFbbNbv*xt;^(B#YbVwH? z;6q+H;KITV_@mYo;y`RfhRP|eVkC#NJvod^9CFY2vPWFABv?f|^ENZw$jzh^YL&oK zE|Ra;31mxifrxD`k@nM;yFo>b63CWY>{c35LJlX_X_yZZY^8uT`)5B<%suj2k^xB0hy)f5 zmr(gP!^N3-2P!4|x8^8~d!C^sC({?~>;a~cX>Z;bM_HxVau(lOINNisI1cr;?{%2h zzs6-2K1f^;{9mY8wr(qY@l)%-Z;UUGxu|UgPIvN^oVeTMt_ljB15f66k}@-HORj8H z$WBobvk~#O)*fr?Y_lr=gX10Ehlbo#)0mdDZN=@!w*!ti27C0-nmv{-Tef%_Byl)7 zSOqh$l$?~q-dOmpki5)X3-B>SmYBNYuHsxQY_6_fDW$G{uMVAe`WX_^`}gm6b6yN} zr3TU`_+<{Wu)yaxJp_rIOVbut;~Z-C4t)4C6*+K595g_QtVRB$n@ED`l>wya8Tt<( zihKov-53JdF{$D@<~%K_zvNPVhmv$!^eF%!=7q!~Usn8lvhe~6ChQxQtU_=&+N95B2c4PFEv+2aS6j;w|KnAbe(6If) z)qN7B!R8eUNuszSMCN{SIu=`paP+~GCnmj}2S@bpx1-OV4xUFXi73#b84 z5OX*}i#NqMQ*GfEUUZyWJ9(FAvk>j_5h_8F>w7IGbe)S*l3XL~49g~XbQa^^CO`c6 z0*Tj)2IA605$~_BTg-6fZ=C_Uqzoc5+7CJ;F;~AW{t()th!_P~g?TDQE5ltYmeN?h zO7pZ>J4)hS1QEqjBvJ75>FG+D*<*jyD;UA)#4^@5_8&QsPe8DzwO`P#|YI1}LO<8_P_r ziI@S!A*PY|zwd~rW})6uT4^CsYWy*G#5mD*L(mBJqN|*Y^%CIh<0Uk-{CA^!2qlQ9 zbh`GVDn}JGElI+1Y*yWRT1a>Ji~`p2`F5b#G=u%t!3#GK!WO4&bVpnOqWHhsd-Jd! z*S7CFL@lAriUt{1C_{xb5)qM$V6)c8 z3|DJ?&!Xy#4{6tP$ACiC@6)GGA$THIg+4ZOHo8y!;1rxPByU8&VY<%>TgATPbV-{a zZ7=18kc^VIrVVEZ{sko_bekBQL9a548TjR6v^N>1 zscC3wf&&GJfSx#ha4ee7S^@s5*)yjK_kwRKX+7f#!`_}Q6gxvoD)8_3AE5XkBl9X7 zkeLZCqWzDWY~-wvBCL0{Cn6-U%EsSiM9LFtYiFFGIR&klx{5%7G}3AtcS(}4xww!NBH;)a}nNd_N}7yXu`}G0pB|rcM{S6r9+`anE9|lcwE* z?Gv5GMVBGP0=zE}01Kf$fVH-HF;x8BtvXTWz`L7mz(cq6}buF(v_EO|Z zAOOk)p7jM?d3kwZ)3B!v^v1I^e!WB^ZJk&^K>)1TL zR2X0)PiW7rUh_Sn zP46^kiv3v*ky=GNWt&_>gT6mUuQJEZi#|96LmF$};m6_*ZyKi;BkT9+n)zDr_?YlnxKgSmvcrGh`edlJT^M}oM+wb1LJ$Xp`(n)^%RAtsTPw0>Lm{9-h zxxqkCm83U!ikNkQqVyRA#+Kl|M^Rc+{UB{J5{y??(oM?uWnJUu+xyyQI=^k$>36tb zX};0+oCN9fyC$dLy!>uy7Isu29|r3p`f+h$jLygi_DW3_?a^$TQ~Kl+ZFFp|J^blj z7wJOL+PzU>3*!EWyisPFBBwUkLi4!JuRe3Hri>e0o*bvYsJK&5v;Ndl#R@}eS2wB`N?0psQ=ReSf0CAb<#HR5L?UZKGR>U; z`%Vlyf+8k7cJ#W{6xoQ|SWq+bb~u)uqOp=6r#mRenIClTZ9Kz#ab5GR1nl>eqW&fs2982i;|;PxTD@Q+ydf54jo;F3;sQv>V#qVE(md z@gl)EDM*W4V%G`@J{)&{w_A%3QJfgRgth!FRFo^z^r#U`RZ}4|#Iz%5 zPa!ojyD14#UcdbCdS^@VzbmuVc6?ba>S{Q}^+)KzOTvrB5m^|+Zxk@I3$=B05K&wr zu!~8TDCK-t>=*G0y}H}G+^C%kToTRQDCK~|qT5OVZWwWT*R6^R0mCBaK`+2aIFg*_ z65HYWS?{B|ptLI|kka{tl*c^X6_S$*22YMJAqU!n^r*AJs6DMBTFI-=#iM$c&JBbT zgLfY)(ra7~dc~PZJnR40Dq*RC_iYmWww4*?W0TKa=qr6~>#Zx+{_`6pg^&9yTLJrr zx-D)KUTmHB4V{M+0N2(<9=7uyKg0lS!cK02(Nw)sl!q~ zozB1XJ>~M>mGj5SK6$JpyyBRtm4Ye8<$@uYK)KDi+ji#m;olA7|)Ud-?VK;LJ+# zILo(f?=98Eexu3N3FB7(hz6I19{zu%kH-Iw52Y6ew=C5#^p(=*KD|FTYYr_`xqDW6 z{V$o$CH7r4IzQ^TeMffa%-qoCO(x6J?8iz=`OgsLzLsf@n+DAvqb^&q-(A%#B{A>z z`e!oxoe#LHO1awiyUs^irhT>@_AI7L*p1so*D{^kN_9E$zrT5~Z%f>bh68)cX4bU$ z?9#k3JC>2%uKQXX8tXc#lGP3l3QKc+|tj$E4#S3I6r?gx!LCC zWS&Z-!bsfF)_9Y&Zi=?B=H@8 zaK)`<7CbnJ`VaFDxWMDT|1KJsLP$cAOjg(-8s*^Ag1Sf|YW~1Fxn&LUjD>}Tot>R% zRhNI84jHstT^MC8Bq~`;b@?rES)P90w1k0Gerk>4-{<5w`82|bubm(XRr!UW$q}@V z47G|PYHx3H9FEn04s4mWwl>+63$d{;OeOhXe)EUVOBK44gh4xM1S<9IyJ*3JjfUlK z-)`Ve_*FQz@AJ=~+-|t};{N?91B~V=xV3o7YMbw?3+&^g={3bCB`z_fA->st$$FzY zr>2UVZ?{(F*XMiRYbr>nO=#HDkmS<+o_Fn3mw~HGX-U6w)hZhD^6|M>Vq;^SR*0wJ zGTwe%+_3Anj}|5hpDQY=yykjeCx9F>U~O6VFqxl82|M%rdrKeO(Wh$IsOH5r`~I`< zD|e~uS@&DYE->X-znxwx!Qu}~;oe$}@nF>27 zP&JFPjyGxsBFp-X+}h~5|BQs3DcNyfA3^pr9fvhb|onnsFVW-4)BGMk&!_&B`I1yrDdttrCrA8aLx!9D&;01U$12LPWtEMM&BTd*ln7X>ynb`|@dv{7*z@P- zh?m(`U%TLC$>zlKZ9;p$EnL1Nyh-vr?LynYKt@GJTMg~ep+g5hHE^D7>Q)0?-S~?a zEzQgxynH!-(V|6JirXq5K7PCzRRN_l6lI1QibnqF1kZpq{;5Lr52c}fQlaE1R2y^; zGYrd0O6Jd>FY8Bn4dER0ZrxxCVzaZ4)3-1vXr12R41?Bt${g}#S6A;&RlOQiG;C|k z5SSEDqJ(_l+O=ypZg~Ip+c$EBf*a;jz{!4oZ3+Jo8SXX>FFj*C@8QbJ504B9VT=TY zK(K(_q?Zx6A<0f={LX;`P_ArbGL*k<+?eBP8R~#pfwwRu+4BDV%2~5!5nZzp;Xlb9 zy+QJcFq4+L%%G_K`qe5mv#sg2zJ0yrJ#^{^7L5!)_@;1q$lW_~B_2vjN(cIlzYIh8 z;0P1nPsB4st;0~?3R&oe$^;DWc>sW z1WAZCR?aWOy<s`H3rw0nHHy|+%ueJmes~L_$bW~x_4i(bm>Tk7sEX~JOa%FiSK{^eI$t< zBu1>iGb0w!sn8b;o-L@0oSV(^<#dH4F10kOoiimZ$bR}3hofj9aLN|E#;CP*VNIyBU{>CkYsjwp!Y( zWm?SQjI)YDnR_0|p5fc_@?l3F9=DLX#EFcHic%ExTmMPC2a1#_zUkDtGXo&WVc~ju z_32~p>}*F;GCTWB=|$st>sOqvdJ{XPIIZs#R&vqjBj?;QtFm))Fj)`SJXmmr9lWN- z#w|mJoF&4Q?=#YP`t--#=gXIgk&#n00@vvc&#)moym8~kn3$L{t%IsYnzEupVP1!G zEt}^0Xvo!UMfMYA&9;fowX;r5`dVFG{q^g*lkX#}qE?Pptcp}q|NA)9RdXEVISvCk z4ijjrI&$R5-yg0VePV{f4t5hM$F#!2!lzFy3_@?-ym{%;Y%??eAzjqe$#(@lTuJIW zP)(LpGF(<>ZZ4!$a=oLiEsydE$An5tTd8{qYeF&b6{LBLfH^S(Zr%Oe{m!-bC-|vd zjgRj|Ud6_SrB+l@l04Xdcw_15=_FmKcb>>1v+GF-u3Z}y7Z*ncLQR&G9ElaYJBL_j z^yueq^Fz;*X(SQ&6C&hX$d(J|&!bt0t)KG(T7tERj*f=+Se!L#TP&KzuV23G-oKyl zb(R?8M;TKBAETzW6w-*i#-&S_%ntP9&8*+J(P^_?r;?_`OPO`9^LUG$2h^(+jrcxo zOXQBN4<0{upxO@JvV}elYnLr!FTE)#8AtsT4rte|U1|7(@f+^uaQ49utyflkmkOHs;VowKth z-3Vf1k4_y}Utgb>mvSPPU|d3kwZK|$q0nZ-A^{A4dWu3ML!D-}9+&d?sD#YuSrMc6cw zBT{s&cRQ~uB-&vRb8>T8i;X{|XWD+;Fnv;XS$Vk@qL}x8b)^Rbr;O4F6NSp!Wn;X? z6n#1!vTofv3jXjiNgK=OZrX(FNZ3#f zT}c2nGxS3A3t0cw)XAx$%NiRE(83*&W5lnJ>~&L-s#%En;?%;9YjDc6v{VhZ{6OJ< z_1ZOIY-3^!RzK;(ty`>-PS&9P4qbhZ1NPy1m=hXWI_}n_Z&Vahw(%P*Y}uz;wlI~R zIG(F0Ey;v4Cv;L)n@j`iQ@?WH=wqv z`TqT5*1UknR1=7i*!A^ZkfI|-cyHPA=<#EG$C%Nh$vVNJer2tyONZS>8Y!whL<rI zvWwPV8?PE|Ey5E?wd;U1Bpt#l$>){1{8yK>iuHfY_v>q9R5(GgQCn zC&rjXScyq*cvGi*ov9Je*{388r^C2kyLq#j*7Yt=KT0Q;5SWRvnY3vVVqRJ(o|qB8 z>Txd<1(qu!;sf;-v5S+sDJvUn`QeH;+KTEGg4}__$Aym0r+EwJ%l|nK6oH^`*s#Y% zMMY1a?lK=H`bf!H}_m_=AEm_L8Vu3bWk#r~+A z+fSdyBB!RBKq8YGf^^MdNLv+a3UCPs!Bu4alabc}MPMbZ4R$+0#tN|(*-$Zb8O6|A zyYeuz%k^J$dlXx|5UJ)NEj|1T4&a90jOo-h z?D^!>BSN}!N?A#X%-VB8^Q+3a_=pV~79uzh96SYzfDQr!ML_9Djf{UKQ zzhXYikNSF2X{0h6e*UbrAN~3!{i7%jLFqAQ;J`EA+$IYvAR{wpP8N1B${M>JyL+GU zyve4fC8ebgKYSI^`0*PC;@iZN98W)_p%Cl8~Ji4x-<)o;*5 zT)J|FF7)<3FN%r`$Bp|zITQIIcA!VaJ>R0i{2o%g>(}*Bo>A}xLhgswZhXP{Hp3TL0bEUoVZ5VLm z;W`HgTu#oI{q6dE3D*)4) z)vGmn>>-KyWnt&8U%q`?bY=DaD_6!${}BER-^?}FWnj*x!KbaFE}T1;_}ixg`}cR& z%2kl7b-3pB>`n2Tcki;sZ1R<%j3$uE>h9gYk!Nx@%+JmiC6B(ozMH+r`4{cxm>x(? z^;x=fDLV+#=KcZY{AJCek`w52;=}@DW2xx}CYp+*0x~bLkICDYHI|Z+S%e=#Z1>xX zi`(b_XlU@?vExq^s3a^wE(9qzhPd$XL}VBsFZ>Ko@{@*2^Cp=twzYkgA@98}B_-wL zNiiaFX0X0u&z?EWe@qNx+SGWD%0IFBbYzvcF&>U5;L<+T*7LYfqL8nEetMXbvyo!A zidi^*(Yb8&2}iymXpeux>A%D3jMD(2n%Y`MtrLlzW`j98UO)zZXe!=?81XxJ4TEU` zILCbnQh)#~5FfB|<$H7i?o~wu4A$j3~LIQ{#V&ynOjN?@{v@ za$8*;ofAPpUrpMZB5FNz>Cz~Q!7*OI0ipox=kJd{U<^TPEHFyoegw-nxg)7+gg)jP zfg)hAwm#R}U`;!xfI}P^t0;Zih#@fs2vvijF)3W2V=+IJwm+SJ`GpR3S168&JQCh_ zARm&Y=&KTV>XbJX!%Yo!C^>TS^C!^rf!GU7&683jF@F3j0z~F6iuNlh1ILZ?1l?ax z4ja3fR4Q@jgYOQ;6fx*%Yo8mU8=d;u^&y)*?am!9?LisJqfdMW4{`ni_U=UoFxRNX zo_)7s#k=xyyjY{CE$%nYlJKK)_Rt;}fQGs%RF0_2e$~{;U-Fd+jf*ov zmri6)F`rOya*{gkyh8%+_U+rbxgL~}L*N9Z%eoz$7?y|_Pt-;=H*SJ)k(1ktVW3{+ zoV5lp3eXLu!<)myzF6ZMkeHlIS-~9|#ZXyG??gTr3xtI&`#KCY3;|Z{>HeDRD0~vP z+QbsB8qMxHap1sAC#M*zC^jZt6q|WTpm1&F?j^w_U7sEwHGs9ko+GYB{D`KS3zWO% z=^EQEU8@0%Uic{$zLlk6GOHPk{r}83J&i zLH*RG4(vuSkKZN8TMN3wR&Gf8bC#BxCa{BeydMou;61cYwf9tm`g(9sRcqL=Mf5CS z{eaEcSx<6?g@ue;znmN^BCd{|J4ZZkA8G<5_aKbV9yS7EKZ%c`#SV-I3`fe=9*C5< zvcKK-ZJ2{MW9$R!GRAzRKMNyfn!u|K3W7?R8ewI4ZhImG!d&)Ub~Zn_8#=65vBJS2 z|67!A!|yy3t#+WGc=rtr4GGG}rVfP7L4PKi3ne%|efrc!BTzrK%X-}1Ofm^LFWYB= zNAv&o+QiZ^tOY&+lx1zC9BEfkMEFmIARpj%Qz4ERcgc0nQS208S!U)aeSQ7m!-tO^ zeR!QzI1tC1jg9*QUJ=2i%uPl?*DX-f6!P92pgbaGvyBu?C~p|LW$7MG(=$;~M2yqR zcDezp-f!6SX%xahYFdVP8TNHm74h5j>60c+s!CoMi?_g+#Kt;fcIV9LKC7za*2qF1bP)Eu2ZKcSJU@+D$T^d??+_-LNk3BGZz}R_CX2^%J zn^aK8#Z@86K)tGctV>BS6lI7{Gkf*vhNJ>`4W8ViP~%inWPeT07?-3p>Xi^sxqb%? zaGD;I+s`0TS;h*ME2G#V>Da@RRDerEA7sB*`xV; zH8ta`Y1=fD5D;MlsW86?IHhdhRbF0MY3au1oUFXOg#*6$HoU88Q~Zn zb&f{hmc|ZUg;Jbwani(zwEsz$+Yn+Bv3KiP$QhoS_=zwzc&v+^f9DzSep_)+G;_nA z_Se?7#&E=9PK;n94<3BRZ*vIq4p4LQNldR1Lzb8l##>+!r)=-PEjC5G;*(=(v?QAL zK;2qAXrt1F^CYCm;o)yd!Eji3-;fq#M~?ham)bHS&|J$l`ONwA4G8nA?c~&vRsM!) z0L+Ww(!>fE6#Wcuak?Hn_=G)w#bo2fn(`exBEp2Ug^!@6$2huJ?Cw4UM3~p|sB_H* ze7&C<8MmFqqw2c}9uiu9#0T?)dw{(Ak zNOl^|sJZbckbhb4UjwFMQ(-MAR>qx|{FXVEv;2e>Rm}2aK`5Howrv}(v~0XFaENmV zSmC^eDs2zY9y^weO#tGcC#y18JV@Eo$h5RHN{w>fOjE=~85yNgEkPK;T!+6_jwW2O z_#mD9ffEYkCm6d~z5t8MejyT1Ja_J|SFe)%ue7PVO`w~eE>1rIISs|K*8BQT;bBNZ zu$8DT5RCzG3RHXIGPZBu&Ut3Hyn5R0EuoxPblJA&cZNPAu}bUPee5MpEgg^%$3pco zFmT~Qo(Nq*MG;ohA_PVpFknFB*oja~*M(MJPA=z&qK>kR=Qv^^;?@;w)*NG+0r3$>o!_qA znnzAS_~^zC>rb$6Xyz^b-Z^nLKRkcFeC^t4 zA#z<$P{a(s$Vih*M7HT`aHAC8WY5|!ER`Xh0MxifjBtWdfZnOBtONz0sta7bszX4K zsXF1*3%8D;oBuFh1)+#__gU-WVnlNUoMR)$m<3Q#QStCjdI1o5@uKp}#am6Jf@@(& z>+0r3S@YikL?ph<{5!S}*Ct=h9-tlC^ub5OYwi2*pWMssgC(bv}*H@W6@O2w_oeC7Q zqN$r%_GYn-DgQ`JNhz-V;6<4M`LMEAXKs0GXlX&lvQ@v()$sZA!&3*cXcaPWb5!*Z z#(Hkrv{qwa#t6|)3S2Za&|DN6OZxJV+p`VXb{;tO#XiwT00q>XsIUm^8YvZ`RJud^ z_OD*PJU1_ehIV^_bJPj$+qd$KYrta~B^6UJEF|YRLux{-@OTFg&Vu1%)G%>4i3m=# z=c5kl>cqS2*5L?%N9|1rI7nYs>WdKk!Ra=IA}s_&6Kd1)Xd*?c((v3zkea|VA+;72 z4oSH-90N?yUV<;o>1s|Q7~+&sh=O$gL4yF3LRUr>aFvpRsfkA5i=3Qm-kVzMAnixW zt8^5`fmNZQ$~t@dm!NR=E&a*P#0m@mC%SOaqO&tjTwZ>!BUwX>A?=etAt6mUefk|d zXdlvT-MeQsXCpR;hE02SL!m2%1r{eeH@BsEByKMFVY>oVtXR8t+bd&Nq;J1b@|4pg zx@6SnehOTb;yx3lc0F`2-M6n($BxUdX$4rb;20Jz0_)SKS9p4Q z&YyoAhh%$}9)~JqfTQo+Av={9a~==C#om&-Wh+XT2Mzk~NE*WxL@v0lJqHdjk4$ul zZb5RmcIa>t`s+`m9 z@p57`Hj+%10t(o{qHE<0BbiVW{hXa!&B83fk?SZOC@pP;IE5R1!6pX{CYqkHikdO_ z%EgO`xi2ChX$jAO$k;VcW0Fpo24%yov|sui%oz7vDq}h%3Rz=5&=XO>h?+mBQKzgA zqG#)b<)OJ7+}%~iteydIpe&9Z)~{c`wr$($8KscYa)FTI^<-pN?td?>au;8QQ1RuP z6AT8M${$#mO-dLwy?0YOBp`C((s%F10FZ%%Onln_8hHKsHG$}r#OY}y5Xr5)zr>K*P$wh1)2-I%nR@QQM&nec0>KP_+7g?)HZ!CFMmA1q@?eE z4UA@XzOEmZYTuB-@>>r+VgseWW{t;>44WHy-q1Ao+M72Gy*0g3OG-*Ueq3#$KqkMv z$qy*z303Fq7A)8VRY9jmg5}7PiNjd~vWpZCnvmuPCjgeQp!{j}ix-rhC>w2U&D+1bs`<0tTwI}QbT4pPw#=nf-lr)j}71{RhAIN2HT~iGMs^YEw`c2}dZ>t#@ z2#G5c%Cxn5l96$-!kS9{)YKLj6~>lh3D3FpSLgM?+>GL_yAx>$DMw~v6&NqMG$S0 zoD%PK-~Rn4I&3ri$TGs3Q&U#auviDG13cbBCYla$FN{a)OW`MalV1mx&2w2w07>sI zfe<$h_!aC((o?`WB^~ntgX3r?uAqc6KY!pFDGW)&pWNKm->&vwD0uv6rF(>aN)Gq1W*C} z+3@3swytg^$-wdBY0&tv;HAP&!YUjKlnBwOhoJ-1qJ&di4Agr0@~?39l2b5|n2fDE zCw3(_(XHFz6DP#zIR36URkRBw?khjB<5oM;4i~8HWkaGbn>SAk6J^V3YG`0GE8jRK zx`>@X&;)_QnL@8}u;2J@3JPKZb(iS_qUNO}T)EO$-_te!eivgG_qTU{sa4I+KTR&A z*)IQdo3{iBTrLzd=hn3l9%N@*&zqMJ7G|P(ENhF&a9oqns_?8XAaewdn> zAyJqXVdd-?;I|m{$p?+VGP``|1n7YHc-Zdk-nzZuDgF^eTA&Fk0$eJLa~LpyM;&)VG#ACH=NUnitc){V#s5Xx{-n^yi6Pc+DK| zpOxh6pZ4^r6HRIzukSTXgI9g@C>mVH^{sJHN?8z|iyi_bZ3m)F zO-)sk1%*-vYM+|;%9e@m$!5>n$cy3;F=HM`iLHfefBCWxjja0g`OB*L?L@-W2hX0l zU_VKU4F9EK*lS9fsVK7Be7(~XvFc;42eWJnYeFt&-V%W%r4&g!+jag;J!tQoJJ z%H%`zpzYCPC2!~S={dHx-kh92{^$W=3iU@yWdZpmdbG7Y<0u4Kgzh5swNy!-Uw`6+vhc zrsF|Ae|B84B2C&ZAe}5}L17^i9=E3i-@v?*Q8qX4823}*5LlB~0hS~tI{MMWhtq6J zq3lF%K0cmY-70nl+|i1onqF5h03^Y9>xrG0_>E913far%H~D#)~AgQ3^PHQ^c2}-MVq( z2C^OK*{bAhjxkYXyw51$57B^#~fIsby@)jtTkw?Nh^ls93nw)4l8VJ zCYQ;sm$6@(8f^8wbwPmM4y!Q%H{>5dZ}-SL-S zC=abA6eKDY2q2nUCj7IdW|Zv+-)WWq1>NjTDuB5s=+a{JxOkloliJpWfxh8x!G|L6;j)&NVyAK>dWBUzZ zC7}G!p-kI(ep@R<-@J-z!3Sle)-?`;FH21HB#>$4%MR?>)3Hkz=gr4OE4v?G-qB2< zo;Ek|F=zxX2pxmyd{6&gB3Lg*a(3?2X>ObynSl#-`t#HeU8jdHjikG(-k7)#zeAEP zFV0maARr{97>I-HCuL(GCv?E0cKU9dC@3@D0^B4A2Ny$*tH~EGnA9Z95H>)h^aqh- z1~EfJ$^P;S(=5)gO#FqZdg(}**;)heu za0O&4zHiY8-FqP>X2HZB-lF|D+{qa7HfU8-wt8OJsxQV{p#;CMwae^8upbwldO<;Y zsGwCkO`us-UQ$d98+yoZ|X1e6(x|I6}ZcJJlnFq=DF)Wlg znZ4gG$MFQ8Z=jD3%5FmL*SFfBp0N1Tw$hBFO3oDAC%Py6chtC)aSSjAHzw$5+l)9e ziRfqvyr4D=LOLfJVdII^(BTm}ggu)avYr}5@F;zJn$!De*h;e4_&hErK7IA-7e9d> zM~Q$T7`^5{d(ckhca%F`vCd#%PPj#g+#!B%4^8%H?#od|szidIvR+XXMZwiqtHA@& zpn}K@u?*RYsysj1%xkEthjZX;;jL&DH}KeFy4i{@eCXR?4kFXuCbg=6porKm^i}v3 zs2~aPyA#qnFRW37*yb&ZbRdmw8k?FbK7ZDW8`L&aq~Ti|yIaBK@XgDP#;VEkJOX0v z7DL|;uFO2iNRI)uth8IhhNiQP$P&TB>5U(MO<5q#%>$LIL+Npg)jD$Ys4kVK@L|Oc ziYdOnQv2_=tIUU`hOb@EO9SU)smW+ihSw`l(<|x6k0103LSTZ&g+>Kips?`p3=}O` z936W|^I^Zx>nPLGDl{J+>BjZzqG`mQJ=bsC5S*SYw{n&cb=L8b%Qy`e7QR>`__|+Y zWsi?r|CZehxVubcEvYKR$^KEt(3YGQaFM@pSI2D_2AJ zzl%&1sxP>?L&y%efG{-qB5uYJ!0bCZIc;=vV>h8PmK9vvw5 zAGU|Q01DRhx@!CYse+h8JB&S02*b6gs;XMZ+)fkTLaaK;4ovD(BtwrLZE$t9oR{31 z!fX|WrZ#>XIEqr(^6IgnFgBU&9Z2u;Ako)XtQ$3$WNCpqtF>>n;uQ&Y$U z<{8YHa|CptCJXU|A%egp+XxlPUf};KrFU%9l;n)%Ocanjv&C3pn7s)XBIE*uoQv3l zM0P=5o)^MnZc$nJRC-{gIMNYvmp~hzNdY2?{P{9b>y0YJ98eRXHT*h>ys`nvZ4{(= zT4Hc669rQ8c}t#WWQ1C4Y&O)8s#!!PiiKtcTXO?J2iocPlW24n68tS#z<55%l_6B> z*)(dVi2&rhh{+LK5Ww|8BCe-Z{eWA}y|7>fdE>*YO2z=Ka1SC?QmiVPO+o4DC*t$4 z#-&KzR`~~($^HXT#;HbTHe|2pE z)xvp_=8F(~d$2)1nuMaFW=;%n7{21z!*_{6+Zg~;!jlFY`P^i*NLbjL z*uYAp1I#xo_=b8a^z`X84RwVSLZPCKV=5@%LH?SWnrq|X-Nr2LUU?6xn)ahl4A;;Q zI#x1pnU7OFqzi5E2UwjaGDuO5mW3Ne{eS(!2(Jfr{Be=gyS? zKVkmsy!0F)y-D~X#aib%7@RM ziN>pOeB2A0E#}*b;ur}s_z1i!B_{}Iu?q>@88}fw)KcO$6TKuM7I4*1txw;+S_>}k zC^l9TWDOV(<&4PvPyzuV_Y+W-l9>49Uj`T<2AC~tU@;%T%6x_voWc(K zsGTjnC{?c|3=0G_lB_FNuWq3enfibM5Zf@EK-VRkKJ-MI!4LvmCp8$)a&za20G(() zncR5(zyjDmIlqx0gZMUl3WbIS{=G|{a(`mlB8T_w+vO$-{gjmfIoOuF&%;a72o_$Z z^i)SZIk4Nbe0M9*JXRI(jUoO=v^q5K5k+QE){pGU6n63^_!|0V(3anMtfdfoCs;vohjVB!KD zao+CV|A<-^N)zs%8jsxEvD(@!k4SN85VmqwncRbE{a#n+GC}juRFKl?@Nk4GsIgh> z^VS-Wr`mmbmL#)vdfR!L+<3ERApjk0(%z){`*)0^T2`K^zrbBYjph*&!OC35%7k>` zRdaQdzWwOYI$S%3<2w3o0unZw_Nm?_hcS|3^ee5!gcghBb9q@Aec{gqAEEcJF6I@2 zkWN>WqEv6@W*AX;V_DiRKq7}@pQ@3a!VZ`ZV5yFg(GrCnBy2vGwKa}=TT!8`pb*-u zoh~q(xS}|%j0Pfugi;VKzg3hQB`<6bq0QXw)y2}@zQlIkV;x;xcMp%;oE)ZkAm@Ag zZFPX3wFVn>9YG<6pK=MRs(=lQ_M+wT2A+xF6c$!h&n@7T|Gk}J5eQ~PJaf|*FO5k> z>B@?r)y? zMHd=@tmBN#o%6SV@hK7j17fnvFS=w1w@Vsd;_r?TW&OOotL@vZBBl_sRc-r>RtfHS zwC!lBFh-pBRV!B#QZF8S#5Q&74d|+uFOM7ce%nn^aRIQ7!bFG@2=(~=nbd;N_OP0e ziAjp~y8Wvg*p)1Tih@G55YDL>ENuEC7MDz}ofINr6Ly)J+ELgbps|qH^nM^zx@1Da zPM@YrZy%U^)Erd3nkek*qNj0`0q+f$78}sGLZWd z+J<@_Z(@GV(Lj97v7CpldcWg+~$28%+m<>Zr9vb?Z6EkGy zpRViIzv=B5FjXQ(-@R|&_3{NkQX!dvMhOr1GoNQFs#%!V@;?L-WkX&-2-3N(Ti32( zrDvovQmMZrdZSv{qvQ(6>&ernCxe0j`r3h=Jr&Tf5XaJ%aYpY4S9a=2!d@s>ES|i) zJhV4XWVh+_t)q9KP2UGLUzjznDbKR^p)Z_gr2 zfN15Dq59;Nv`N9G?&sSpfERoL@lLhr&P2|n5yFt~lFP}AJ@om*2OC@45r?L7&O|WX zM?Mi&^?N?d7Y;EZ_C9v*KdA89W|XlkNl(gHtA~=39*G-{4XelqJ2Bqy>C>2)mo_c8 zc~&$3u_qOVp@$&yefjDYYJ+1(j~1a$qpXTYLP1BDKblJv6fC9$-I;Qx+q}wVTCXme zKVKg)PQ@&;WI}?^QH1h6vFrI8!!L2et5fmv*+<;TK!VmWzsWa0DrfX{gqfyV$7$`_ zQDeta&DImV5RnNN7#&Rn~3Gb=zhX3{>=U^xVOD{z6;hVXz9{xNKSGcEvVQ z6HQHbkOu+TESmrUlG(^FlJnV<~c}XEQgX?{2AI- zy(jb$E=E-~6>T;4gcysEjK0veNgD=Hc2B@YQ-N{3_=f3>x44^}e4}5SnXwrr6|Mvd zVBzJJyCbhi*)n7-iJ~S595iM`(xQJFx_+BfZ>3So?~>-YMi)uZ$Db3*t1mdRSkuMX znO?Oul*3K<6Url{Jc<~5%H+wlOQ(e9vtrz0ZB31~vb3i?&KCg2g%aoCN&!2{3vp|3 zD$0giUXtpH$)^O%5iw8X_I7nR8Ze(dXxbQmOo^L&ZHVaP1gP!O&XCvB#f&lV(<1U8 z&n|7N%qs(Mf15t!?jSwAg$g?u-5`c=oO(uXZ5=WnI5(VNcE^%yG#sy}s30cb(hGrE zuiPRqfn?$h08<3%73Ucl_vnzmKj*g$f;{?M+n?5OT zO_}0CW3nKo`?14nr{76S+vMawf5!2Vw{P7FT_kuO!|;)w^Ut}p3DpYNy}PUQGFYHG zB?Z!>5Rb=>eJU@{-=5^_Oq0;-c<;rURrP~NoNRX6IT=s8(qsnE@5tc2jfc>;bKG8J7Lr7zKvS%|p-T?ZhY|6$N=HwtEi zW>yN)Ft(t5QmlB*;-npBJEiQ2qKJRl+&Ft;QDxncHsG*iO1X`VefRITkaYL(-nUqj zVDK-*#^=|gHQ^;t(t_@cX#&*seBjma&-d?Z8gBH-=u5`CjDcU~*dPlFxLXVfV33q{ zt1{E0u=rr#zP_v30LDu$F2A>jUX-cSBXVR20v&y#+4zW>2=coRnVD^^FC~?+hPKX( z!)E6Abc6m0q@9Y2inteIin77EC^)GM5xps(FZgI9<Q~+ zjc2g!_e+fis^YM-XFmaVnFKPg_7JSd`4(~e6k-H%05$1PQZj%wAglT^BSY=jV{|#( z5`rukcV2{-zqW;pbY%$SCkbil*~uB>2wv!}twVs*qAkdO1;NVDlurrau=y5og)z!4@o$M z3nA7tS_}Q8 z*|kS-Ihlhoye3A6S2J8DXR3aIYDdo+Ibp+Y)E@m88^X6s&CVjuH3~6=8Y430G2@qy zg+gaA7|MTiU4<1QUrWpxbf8lKO;9NEf)64B8`y0(a@V0fhQE;M`ja*Iu;C|;+9P+( zSY(TD-@N%kLAA)6T^OpaPA;f7@D{V8Aei1TL8%non1BbuP=dHwaCmjfmgp4l_9J}? z4<10bprbVvAXUP+lm$M8O(!E0>{a$_ z?H#$GnKEuUM0DKkM5-rr)b#a*N{twZ!)>70N#2i^ICp6}u$sIOU1OYmE?&Nj1t=$l zBs!Ur4}^#lk*mcZ+UR%(W1{fhj%Pu!$Pl<|XfO;zR-0{=QRmO65FO=zx5r5G@qnl< zfWyw6H*VOlJZE-5fT{XyjX(&)esBl2GF0rF7+hHM+na6GO43s5b1CJ3*g1I;b{u9H z07)cnNw`F6oCGqLC|sosdEmf-qef-EdbJ>HQyVGcVj%^GF{1Mg3Aq>Vdr>zdX?cSP zhg!G{_H0LH;k7obU@8Et;C(?U?Odp8K5>J!<{oo!rp>%u3(pNmTS5qtwf{Ln42 zU$6?w>F9h?>Bz;Mz!G#Xbq#r`(pk#X5l2NX01(+sB9$uL!cDiRH=sXDHb@^GGYuV% zPGN(~Pi-1$2|L~}J_*lGgj?2Da@e>xL?26x)H;I1U%h%&HhAp#@kvw(-n%!5Tnw*@ zO^3k-&Qc&&aF$YH|6RMNW;Qc5ZA93K)^xOv4y^ps^z z^~Vy0WB64LIlY81C=|Byh>Pc~gan!Yj=RAD>68qVGa5HP+#-MzZ(9nf2g>w7?rkYYzYpy`%A z7lWf{FAk zriF*pavfe0XK&}k%)r~MEg|ytkCLWfYVO17^QZ2fdqYXZb)Yb5p;|;G$T5Es05SKo zQm6k7@|ON{-k6AGm0aFO%p(@U4$!FiOsSgZaj^z>v9wTEDWj26eVE-kO1MUsNT-GB z^FL3W5;I}hXM*&=f*TSflW>QVYi1G}1ilY=GB9#Q_-4sx(e{b<;*|1=ugjQDaXJ&w zGtE>-EV;Q@VFx3?O%&i|u}WNQpHELtyaVBsl|^SBFQG4$l)^A{0^&%XmE3QQ9p{!P zR1k4m2w0^}C^)q7A&c{b@@CTYzmdscw*+HIv>YlTVB(sMu;B#42rs%y_orX4PxB`qct4aBg^{OATfYEF zdmuC?)1QVK}?5o-;8xdvWoR*aXtK>0#Qim9YlbGA2Xh(W`L zJL2jE=y%jDkl~!QF3=JRK-htpeBO0Xx?h7ZIjvXt?eF$~|EQ*<7xec*k$N45C$Yf$ zBxt?@51)SMXXdx_?+xdBa|K*3Yg?GSW0&RPM!!WQLnb%h_C%W?b!eFk0thce?OJ4|3^|d zB)rpZBA-_K1?g)g8pi8zB`CoNKVB48 z3E(6e3@};~)lXMx@U&F&e#wm~2>wPi!l5ZCA;|7Md#vh`M49(tQr8rR&?69RP9n=9 zQaBHl4w83iT*F*GdShRvh?dx;KDHgQjm>Z)L?NFDWY;r0Fs_qmE3*@y!%rj89LJd=`OFU@0PH7H#bl?l~B;DVm=cl(qncTIj&2_JI z=qE{yYP3v0$~nJjJrfO^@;!!XXsm1K;`Ggr#B*O*%f=esADQoKEwm!>0{z1=^pft+ z*zKm*2}NG#D;`{Hf6`#HJDx2cnw?V$cp%-7r~8p6>Q*7%9|Gq|oBSv_eK5}_Uo+BR zYhxlBOJ?WR*P~A_TLyVZ&|~{Eh5I&^U;0Ljpa@vBVZ&uf;U~3|bxJE%t<3~TnEf6s)cYrCUTS5uV>-qGRsNbqS`@gt%m3WZYlLorY zsPyQTOUw^65u>RA^X!>82Olw|%JaROwxq?Xry9%>GrYaLx-dCw?=h-f4UiAx=R~qO z=gMMKWFR$4Pbh0Y0!`AZS0q(!C;*y;!V?f%k(x$3DClHeGUp`T?V6OKt$Oojsi`1* zpeDeB=PNrT#wJraLz529?S$ID(2ooIL7oDfSyiq2l{-e?DyH=asJET6V%aWTR8ap2 zj+OrR;LZ5`^M>{ib3~U=G-=dW#g1k7ffsI=&+do*7eEflM-O1-J?9PwWh;0)rRjuVqVq?$t6g?MKLy!=h!S7F? zPYuoXbKfPp2R6%H_AF0 zULH^$ueuryR$vIPvf}Mq*tHVEowsklP8dKs4DJ*$EtL(IAb7qD0mh&s5A(o0^1bt( zo^YDwZD|mik{lerpZ0pKs;ldi3kmtYm~aYMW$4seCns-J0g$R)FWnRWYwE&`YuudZ zmI5QM7ZMX2D@5X~4)sK_hT%n06_*~UI2c{PCt$N+9NRmsGq-ZVGCpg<}ybHDvy!}G*avw>T#;7 zT4wdmQh|L^S6_~=g`_f8h(hcL=6i@(Y za3MmSP+MlRb0Ybo0Mhf!Z#OBsjq?<87xM_xVPxVt^K>#5UXj{!R1BgGGkWvdzmOr) zXBS+u+jKNaNa9K+Q_l2WN7kQ0>+aIG5flLA$eEy+v|qO_h;mc@o^79ZnG8tkkGFey z>2tMq52HsF;0wq!gO+iGLo^n|x*J@+bZI>)UhW46OkEtZKj6v?l<9=DXfE@PS}@{{ zy{bKCj3`cTOnnOtkcwdzQT>f?=@Xn}QV!#6-N~GQS~#~Lc+SV8Cr@C*E~?3rF?=Nk z^JFx~i(M);f4ZP@GOeBE)whl_$ok%6Y9 z+G~6vV9N=e3Qf8ytDSgE1Z#C5O6u(Gu6tf`V(OOy(<`G-pT2_fg7XR8x=2BqRtqBc z0Nipe{uG`15aNKIYVepOBuT(t2yQ$dJm7*|zR6QUC&a!jO~oxxPo)CESZg47#(m#t z{QB5-afdp)=g*A|x0B1|v!dAmI6`8P(vp#KAlOikFv+wIl`!FSL^hc$30oJA zHvd2iEaH}T`^VA;ZZk5E&i8Riz9p@()j>q+RN0 zTns%Y`QoAfBvyRw-3ns=jwPis9RB6s|KE@M{{K(lElB+@ht2|EMy)t-0yMG-p{a}_gl~Re(PP|^SK zD-#pb61Dxib(xqJ>N7DdNMxCh??i|_VBoJsj{6N=n3&jBkpItPij4k?iHZ47o5KdK z2AT)tEGYI8W|kClD+v#KM|_%zNkQ4e(agfm%5|N&m9>q7qCj6xzQ8&gOGN?w?V6i4 z9aXJP+U!5=Y<1|g)?tg&b{4Xh0?JD36g=c`0edS~vvnT!ryN}5JQM|HuPcYI$+sm1 z*3Evz)lN}h7rEg&15KTEsuX9dbvqm zaQQt<7%)i>Ge^lS5}U~@&3@3*;-Aksx;dYky}6}@q}3@adn*T57kqZhKc96xNpYpP zoTU6uF8$As|IH0Bw3?d#+~Yrd7JK`DZsFp(#~rWndqDofPrDrUbhMJxwQ`}jIa^rm zamQn>CvW2@r|N8F=1OrsOrf0md!lsye&lsqw@OH@+o)+~VdFqvLgZiliq&p2S1UyU zGH>FWrNp;vJ-h{zwnJ{q_M>y3(xh10obddQKDBL&+&1aIKZT`YY36G7|MWRa3%L^% zXL~ceqm8|pwUwl!gSEiAe@0GDm2!&WjN9RHr2h5y)l^k=oGB-4PT>bGy8BetsqInS zu|;;r4)LuLTV@|pQ&UdO!Nt|g!NN*yx1s>XL&CsO~o6MFSJFLw9{{7t)3pXn@m~QP*+r8_sM|4lUwfB)DUzK}TvK-0QRS91j$h`8&=35)XQdjNX+r#{g zbD!8nxx2^BETb1rUfTM-#!L16u00&gHw5;~W8NjVf93o|{GE)JskouGa5{%d$n{gn z-F|T~FZy15@fqch8n?2J2{jPonoYD9AF!4>%zkxdxghhOO{jskM)xF2CuUoTj z6q;hslUGX9!yo7KE{~J4u2d6_aecpMRdM)iep)v_?^oAjJZ*n~aO@lIc_q^rf_=Q6 zZOV*^s&gftzkVcmjrK%HWxut--N_F$Bwsu&_YJh;QTY(ZyunmB&fB>$&&QNDJ@WMF z)7z^4VPA8pjXq&aikKZIjF%mpTF-rw01r{p8&)^#}P=(7kZYu}-_(xyHmM_A0h zJ-64#S5Iv|#(J~_c$aui{>(_hixh?X{2G?UEFODiJMxNM*?ai?zBu1sHZ1&HLB@D% zx9%_EP1LtRc04Wj3ak#QtCy8a=I1)Mmraj;lidC+wBc^)b>T+0YU2P&$^5B7#zY*W z);i9|`HQ8MRlQ4<;;RcwzNK31V`=`ZZ~C)7hgwP5PoBCghP-~;b?pYMK9<|&#eo@h z&gED6RUaLB9G2eE9&Va9`qi#A?i2Y8d1C7rH@#%1FiBJ2m&~h*&fo3&{8$+?H2rHt zgXhJ_mt%3B9T8_2EOfwoPLBTUjG;CZ`mv3DqvYq3NftC%cBjYo(}RN#_Z0Bc^Jhj< z27*IFL-kEO+aD&xmaUxU&!Kzh5aU;MzE-qMs``yh1B$eXa#O5tJzGt()^vJ!Pk+CC zK;uK*Qs@7~w@_-Af(# zc|%m6OrKB#9~m$qi=SVgt>1g;A|=kHQkM4XW1PEQEs})t%)pg_pSahSyuG^%5mzfT z{*3b(tzBj8dNA+&Vjh|Pd)KuBDawOy*4uqaEIp!Q)Oz)!$+h{`mX;Makv%T07S&69 zQ(4)N&M50zEY_y<(YAdma^fl&vxM>0hDo$LWx67 zidi!`Jf1UC6IfWEk%odC?Z>9FNu>^@BBQ8_EsP#oe|ONWWG!{7llJ-15z)@uov~gL z%hKB_E)S~z@%GsBQ=ZL!tDM)0Cp)QYs_ml{3zUiUM^W->Q%v(0ES0&1jOI@U-k*MCEOD@O|vTh`phoQxlAPe#M{7C3gb=Z zk+e>}zf=5vAD{8^^R8W=M0~%K`KI;{4-cEYx)5`^>!FQ}jb`LFo;^N{-r$@T5FrySaonLlx^|=v|D4=_^IvZO?hh>(HS9sSyv%*8jH_V_cyE&U~9W-MR-!Bv`4Y-FUI zM}L!n*be+T9^3IYD!gc2z;@4lm;Sm&?I(-1={$Qw?PCo!wbk2B>L8ysr*u{+Gooc) zXvZn}DQ_|La-Kf+^yIRXte?-n4T_I6_M5jd?Flyp0@iO3vztH2z|W*&ArdcI)n~NxfTBQX9X+{%etT?vMLXgW{P8 zfE5ql^D)LtjP?ZyDT`^wk2PDwpFjmG;q~g#R-O*6Ih1N%j4W+mpFPqUN2{!;Sj^v3 zlWKv1L*2{6Wt?Mtzu9Nh(-_mTRrX(>+MNG$3Ci(vq?g3RM6bRV88#p9Gmq(ro$skA z;iGx?W_7++e!7MJ4r#G6Mqc_lla zjwNM~>*3@*m4GxkSS~4f)XU3bnr(-BPYu!?wLDBb;9AsnyRgW}U#(Y^H`wmI!u!Iq zGz6h0s8lGCESSuVQQDcc!>TNF_lFOu79~Y924c@W23nA=E8~>&OyYf`Oaiv$$vVE7O7l#mmU-h=s)jIsuicb#M9 z9qKZRvQ}<7{Fj1FV|((V@hZu!|dK z4@X$X{`it~%)AS!PpUIZh+2=aVJb_ZO?R6H>~F&h1r?h|L_~}=cu|o4T;eYxiR27_ z&;V$--X}xJ9oT!v8Pl8-E#3Jjw(rH5N$Qn!4Q2&41q4&fo&%}ofivo+pLCmqG{OWM zE*x{n5^8j5Jl&nN`z>ZRqiCk0qM}G$fS-<~A>H_KMO8%Sqy$&_y40A20;EQ@=#f4e zL%#84l}zg;k6r#t*A9xmsYX%AcKs2O{ul*>SMfCR^?IPQ(#lGSiggH)R@VJztMEQW z0k8botHy)`w|+-z#Mq1lsQ&YUMN&x*A3nsJeY~$J>S5Dl`}t8jcT(Hb$;WZD8PD(` z34DE1 z4qaW{;xA*oMUov~5l!f4obIJE%s3SZ#B3jO%P zYpmggR~$+-$&$5X2-u!GHWvbIY|m3fuH1G0&-TgX$mE{^b&x@=Pn@`n1)SU*w1D_6Q8|Qg@vfReK_pNn1jDWI8twHz?qmUxn*VCV1(E?Jr)@Qp( zH+Wc7I$;Tg*HljOIGM=WImaF|YUO;3T)86P%XxOC@lIuito|<47NLa1dj-Yq1q;|K zo@wpNTPo9Z#zQO<(6*?u+Qdsd@8E3}zi9SXow`}h?FaAfMMjZmyj#3=|GYKZZ9{8J zG98=E{MUwMbevhRbYsPyH;>h>Z&TzT(j-u)s4sUyn(Q$I>Sv)i_Ekm)IVVSfyp=Z$%u%E1YDtt?yNKr zY2@@^FA+JnZ2exrSO*?i=Qfn}xQzSv?&gPFX9(BsF+r(o;a3NEGrQ}5% z0(k2F{5ZVtw)yINOaLaX=971VBuEYIy{z*D%V5dst(Hj0+KMBe5W5kXI@U z>6s`Mj;mM=2!?VpRp(MQ($}awW2L9}1!XTh{T7n`u zZQx4L7H$G8BuW&9->CxIv@TzBkLhicwDPa-cZD@0IaS7yPB8`4K|CC7X5z)^5nC)y zhb2eO099GG+UTQ zO`eJ$tJH~#jklbKI^G8SY%dSbI90nf^vfJLFm>J&K^uR?$Lrny(pwaWY`iDEYUWpW z3R1cQiy2TFfy>MG@hnr0lp3geYI6vSpDmW!`Z_q^r_WfubgHpYYfsp<_4_t(WsZ;K zGkgY$SM?>Pkr_Xvr`v=Vc1N)GkGKH?Wgf4Lmc70~GZIgZ#;7H3xt`-RfJ25u{hCIl zwyUb{wFP9Uj~R=F?Q;JyJ=Ul^P_&};@t8wn-ZX$UT1To13ovdk5;+jaW%=Gr)|G*s zB7I17Q~(^Wv0i?9Pfw2|IxZv)udas)Eo?7~%u^qyZXXHjFhJEHkv#+zRR3e z`Nxidf>ZNa*f=^BGcsR`q?JqLocfYzm?I{d^*x0idc*o`P&u&C>9Jl#>&ocya0E24 zOAint0B(0%|70V>7xh9`I}ywRUZBOv_a)oXBF(5B;(e8f*v+rPC`pOK@iGqHVds&?@x>r{3A z%=TrwOdMY_tNJf}J9cFdO>zj;<|`WH@s@P2K~k0dLPJ8FK?JM?8^BLD^_{Sq8XrL7 ze0_}aVkIM8#^%F45EwtxtCYT77jf^nxc>6@ug|*CDlbzpNV8NkXT%yY<{wTr%5tdB z9$4swHqi@R_fNGUwcOdp-m2{2zO^@M@_N$BSzXjymQ^F+3AvMejuHkEV=2Ek`mr3I zHUOy0lx_B3w^^S?vFQmT3k5@yUuTy`%`*3L{Q2(ADoxJ=< zZRNF%y&n^*PZ?T8J#L5jKuissfi&rUtKo@gKYSkjib8)kLOR1%8%8#n0hq( z$+~n1ei4e;Y^IWzvMq(+VYEb~FJjF%P(u{8avK^N4p3$kb7z?CKHO9DzUz@s8||O# z#q5xyPa6 zP*s#Y(Gi7l;%=11Fz~BhXcO1c8_wtyxG`VSID1>_gKnVXxR8(#N-b)z@PV6GsSE@> zcOx3gJmg2}bLV|%apS{HveS73t*$DjGG-=)@6>S2zJ_(IXUA)9GJ)KZFNQyy%g9E!H>xC z(f%#@9&1$$fFD{`v<;4YwM$7dOy7`)$>~E=VHd5^RhQ*r?gb(sK(Y_;vf7lUsPFXQ zZoWX!VbcvaS(S$k`~*g@N24(j>s!21-+Q7xT%J4E z;Lq9!G#0=$4Z!}GtEziy*WLBLpq(fhMfG5la&Bp60DR^GdgmmX=6VcZT`zCcQsB(> zug*Un(3c8IkeyE{_e>cA;@5M+)Zq`zyoZUA^Y53PobIFX(QZ;O&xB%_7iSX&3_i4o z6qwDO@A0Q2)^RX}@smO;udJ-3Ud1z^_7`PvZ?|=wndt1?v@zK!@Hl7zPb}aSqVox` z2zaL5s=n=)sA1~8Y&>17{ihUDT87qvdq2<-2gLmN@gty9TQalhoVRwgoSQ9D&{+3_4WwQ4 zBnZ+~jg1HaKyGi-*1K1fB2P9U9O`SgSJ;5w0_S)s?n9zs`W5Aw%rh-m@NHB3LV(}a z<CF~xboz72n$QQH2%^Cxkz^;QPDR?6u|ol3BpUKQ4z3obpfYTSOP9> zr?7gq21{RH=wr0xsl7%iCNGL&amO)awuKDfDp25 zm_Got1R++Ercr0x*IJ{-UO%T<$u2ujpzmnLmxE%}`DZv~xPlZx2FY?i zFibyzU#LVohvN4XG>t5KFpq4S>)iPKafusM>^couP`UMzlmjvo9(bl`h4ul3h*c{4 z)znZ?_1$+=4*&RQ_xaI{jSin5eHOy|jnz}x|JuB6fUS<7PS98*PZrpXh;xUUfPzqu zf}sYbrP!uxF%zaLK`qB%5z8_LgcHz| z_txhy&{mLYiPsq9;w&GL+=;i$L-7H(D4Tk~Lj`M}k~h(@Cy2`!z}iz=ENd3maLhxg zh(iqB+I8Mpi5{f`UJ)qN0RS8tfbPrI;hx$dXeOD+m_zq>-q>^`8z>!bRsu}%GYz6h z!byU{z>U4u=Ljw~2Mzlkm@zK>7X42C@{r}G27OoBTxawXNpO8RBWqGbn~ zAMUM}*QXyn6OuE-Pe-6g8m5t64GoD6P$v2__lcAvvNzuSrX!(SVKHn5`;Vmvkhg=< zkc)(bf7jL?Ixn9NMhX}XtQZxP7(oyK|IPkqzb}j=FT_or zPusP99Y$t?;Q*}!9byIZ)8{StQ(>|vy;z=wFgrS5LL0wJx>XB#1#dd?2zfyW3xkcH z2Lnjx+{!N=yfTi*r^FZYlSr>wI4k;=MGkUL?{PG45_o*@?(06^v7GTykijGsua-K| z_Gki&j5MT#S)OSJ${Yu6_xxY~vf;882FX*HBZQc8Y64k@$1;-jQyK_0gYg8Ckaa5V zs!m}bS_GA}pLqOzZ|61BYG++Ew6#%_zwNus6A=?*ywecm7I?k-ESJs@?mx(zZL}-1 z7#3!(YyjVCD6mpK>=yRvm*FfAtbe#mP7^Jw2CWwfutgWp4IF3F) zWe8sO!*_Oaef5fHS(p0!mbniyID!-O_bZU| zJ}AO^&^%AZ0Cd@RR1jJ$O3}+5i+?Ft&sR(36G8j#;T8{ve&YD%DnCCRQ}`KkXz*Gr zDkNe)_^sYcQk6Be@6j*-i8P^*t7~Y8zD;hyOjl!`@Y&~Bl<0i1$m}$cU(xzJZ2tJ*!MMyl!-2Os>3hE5O_yc4axxC$; z^SKmQ;m@T!<5NRbC2{!`53Q?W@j(@|=Ty6b7lyUQs|W!~z_!=$_f*iqAL4v)v3zje zsw`})Byz!UQuUtuOpYSkNYHgaL7~CCGyIE42WQMz1{D)bct~4u`?7h}5C_3QJuuDp zohk>rMa=!wbl1eVMJ73#HR>d(zQ@ zzzNt_aOBb9ALuaOMs9E2r;JFIFwQ=3bMx&7DQ+o;Ht04XUru!MfZ3Vsf5lGJ4f+#w z%HXJ{@v8BSp9vR55;%Rc_c##|wlqc(2@sE^#nSxc7{wk!=fly3WMve00*(hOFCha5 z)b9kX5^_qMqSq+e4jO_33lC-8mw)ypGV-VG#kk7d2xRbuMg;ER1(q#rhKTJYCRb@$tJ>7++h&Fx@c#eYp2RF=z01W~T#qBd% z0-%AFLq{$?2+gG!JS40}i>L}g>iEhe`7TN)7HY6$#d~}d%m0S2KL)pWO9{`4IUooy z+aU)k_`~J3@(vBTS?J0^;U|K({6U>fcV4}KxpLr6R7pAa!i2Z7pX?Y!$r}0bQFtb^ zjp(hA+&UFbqq(k%mQ4qz2%^U=S;(aGZ4~w0u7z`n*&BI0=hTU|H`fSBq{1Ry=ct zLiy=y<=qKd9r%g9M5>WRhW-R47O|KNWHoC$u*Z7`sDgWlkaao1(){$JR{5leDRMjE z)>xV$;+P=DB_<=b+30#(SZgdYh2%WYSk9;H>picPli#|$x_7U4@pj6VO*tZtEc`-3 z?Ql1+@`FnvxnF@ZsJH=9d0k8HA>xgPKgk4eZdYs<045^Q_wB8+tjd^|j5cC-e60SN*> zbE@EbJEv|}#1QhN>E(GKf@kWjmukg5gJI|B2=n3fC0qPotaGS6#l=Uz8LT?S`B& ziMX#tp9PMLyfz`dK*3hkM?U?dIHVH+czLWJ;$lx?$BVIsplSusWC1bA%0&9ilT6SE zm%!sz%Br;Rw*Uyq%$%hxLIo2&5J%*oT;F%Mb_M*|Li+xC=K)`$sTOK43o6!2Hu6Lp(;k%`A0Mw7r=+MqjjYzXuNu;T3j=FN zR6n`S@U$=J+!S0P!(@-z{xyVGNc8??W*={k<`tLDCE|#xw!ycvW^t z4X~V)#I5I5^7d#^T^{u18)o3o#eZ=G6|dQG3Ixw8cU%j;dY zeD;_|NF2TS3rUJNEe237bP@Oim=1BEqo#BiqPVqBOpbJii$0sp>33iB7qZEYbxw-) zW1$5cu_oEUNWA!NKDyT=gVZ~lC3D+Cf4H3$){a%cr8rcuSlH!zo{U$bZ`w$IhqaK= zaQZQtbJ^}bFw&$ar1yiK?wIb+K+4F4-$;J)uZ(lgY(8$7S@#Rr#l2!n2|Dp-C^pFJ zmg?&2m1zRQ?$yTBx2V0aWEf|=mKvG@mw7bF(`KeHNp1E&&{cJz!C7X)MutJ~UP1x` z^_8Dq$Vm3Qvvt60irE9W3@CTC%6Ei2k}0c{FL(v0ZJg}q&;p|8CP9n>B5nKWJ2XMU z?GMIefFMO4k3{)HO}Z}nOn##6dTX5F7_j3{=h6D$amWj+8sCgGoAJ{zyr%)fAcoZ7 zMOcRlHM9&5kp2?601Ge=jGHg0F4$jYN|a}oDWjKoMEW86y9Dhx@9GbL`Y7*6LYI|G zVm}U)@R7F^pt_+YrGi4Cb5vOp`3=oWH1nSBq&XL!Ejq@AUobtCNJAvg$=yeN{$U3h z1kMJ)^%icT_O*!wloe&k4?!#&g9IfnpF@a0vCONm1`#g(3K}zN{-iN#TC2Q%(osS$ z8sq!Gx0&FKTbkWSTPY8T1@|_3Z*Qbv8wDKna)^t+#SS>gyOjuCBM04pK!YwdzVfrY|2W9<4|&VlH4hYZFoSh5||NvGcv4lAIl$)lWg_ zU=}LYN}cCGbs*2h6%}6~YKRh$e5LwpJSd=ON7zyD2O;d?MIf3E#IH*!k0K)?CP+QO zlyS?upMuqKP;nYr#uz$|g7r7FXb_iFd}WVG?729OP8CgPI;c?cg+Fn@ju zR&Wvw1Y_=d6HfpmpzSq$W_tub0&DM5FmEAe)+^$>BULZocfic_^pAMFs27Mm2$!;8 zhn&R+`vGuYpe@9AB0%>^NHyjLatt78`#_k03T6V~6cE%Qt33>PEgmflZj%K9jF5kd zhHWAe)l!K@t;pTm=YkPh^>fT@SR;qVg=>N|-k-(* zeMP<0pyElAsiXrlxiLtG>3F${dB+5CVI=&XLl0pojE`LV8q zCe2Y*iv=$QeW37?>C=lTSlnPI_z>RT(f2}CL=cfx2#A@IJE#shUm0~_=~}s-_vH;1 zRszhkzMcC8%qKC)-mQ=iChPlr^+Pm&J{K4U(fuAM0GRk0iW+FPM#AL|HEILtKUNj?lEm^beJ9ry#Y|usnQ$EY1qw)BIc!V0@7y(;(9CXj}1lPU>#k`HXUk36h zqh1aRH-{=dxr@bkeMyqaQwY-BO^nEWyACiFyLH`Ltw^U zOB2watBQ%8j&`Y>XbF9B zQdA#se-oJCawzXHV*8`2V@s~UlY(^l0ulrweG9xB9EAY8qyxVbtY=ha$XbWYhUD+h z0gP}R4MZ+8bf;O=ecG+kKcoBVn zShoSeF+v-Yu9!GcksKHEZ$nu@BmZW*7WxdNh;3L`NJ3>$K3@>R zoH|@p-l{Bs1g!ipoE9HyU4L0dpsyqi{j43j6~BlVEXz!xZSCE=x8+dla@2CtNRPXJ z;Ik38Mn};cq9L(}U+~VbQ8M%wgx?N_I**CL_W1hR-Z3slFDpN3(6RyvtcQgw)aWr; zavb3yA%k!!iO%q!0QR6Eew}%V3mI>3o=Q|J&?chl;JO?bL$rU@D`G_*gSMhdKyW z_oEyK{7SEsdyLXZsBpkE=ap3vQo?!fAw3mJ>ntxbWmNzk951W@Kk7={#z>ElqhHN*t25vfFJw8moKCoBj`=_}CL z)LTiBDTkQBr9!|mBKjBB1p|t8ZEY=~1U8!$62c#>Dmck37$=DT3qZZqx_H&P8N?{W zeR-@hMUNiHS;(I+)39z5GVuYBpSw!%VX6fsa&JF#2y- zIua5dubHKFV7FrqVDqg)*R8OcA$PSYY`Hf2`g5|e7h+FV8|COT3?J8|tslby0amI- zG>sQ4MOJ&Yoa@Ql6DZ%ZV|{u>;W05jua-y)Ds7Y$5{gI350HfYa0kTjdM|GEofBSw zzweNnh)o+WiY)ggi-81!j1vwZ{&sx9^Y%L#$j#2Utv`?R{;4W>Z}f2YKiPQ|Fv>=> zGy)oK?Iu`<{00<{5Efd7{kO2kCxB!@GF3g3Y7}xED|)$*PXX`;i&%^Z|;sYp3Jtf7+v` zB8&_!hOe&QG$(9Y0YVem{@ENq3DGkj$r0eCtfE2zh7EWm-k}7eWPS$baE&C?=B3qA zL^}X_eaFCH8=EZW(oM z*HUOCJaNw`jd}cZjF89h_xGg^{)HiPJDB`N0QriPnUe5xpZ`RUiFkLNaX zh3F!grgb!6hQVV@!PcZWyObeHz<%Hgo){K3Os)^elgMp0A?Y*7J$>IYC{T!jhL$Q0 zzXS7o8HyIYAFdQ5Y(dHTvoXn>JmHxK>L*yIkV}YD3yb|UdT?JDeqU3;fDWSs-P~b+ z6ebQArR*Hw2x5X#RGgmePcF;1$Rknz5KYd!gX9ER3U+3T_p{KNNkA+DBqBB>@Y9Jk zKLDf{aTmY8yU(8kDpESoe$fngWAt8#piv-5%^rf#ugG@X#OeWU)WPGK@z1qH`oV~D ztH_`PyZlscICi|7IOq1%9fK?Y&&M{0x^db} zCk>{PRKginq7%plCbV!qW{f8^vGncMJZUwQ!kFBs!4 z7MtV72)ZaGpI<2Qvw3wIHWZ6ggF8JS6qoYfi;r)Y-|tN`U)Ef|X-+$N{z&>S#r{6Z6rN;OTuAW1V{7(;OkLiDJz zkR=0w5j+#2@cL>}TZy5s+;k?yuAeOEvQ^!v8KFL_4R3#B7_bPG9{tSLIL&`pP1BuE zkOVD9AxaezEh0A)#@izKT5K;P00y|3>=*Ft#y%6D?xc)lEbSR9OgX$Um!ZjQX?#g? z@5X)i3YeIz8YhPeDRG+aE9jd$?l`JXT1xLq6Ov`tszGaywl)&A7QpkID0->{%_y>k z0hIvp6c3bHjJOS5g=%#st*=+R(3lUSqB3sR=h*2~>Tu3dR1`aVcQ%u4y=8eH87mrM zqIxz!A6VKPGmrdS24}SXJ z`-A`Q*(*)9)gpRzB$66&%gSFe`1ei#&WlZD+{w0UdZwCe?BRcvyy3JRU!-Ula4`csw2bt_RyRD z4+oYx+Po=SD*1MC%)Seg%51fY)mC@EQauzXdjGKFy=dKoiO&v^&s_9-bvr(-6X3qwu zTRgMB*7J3tGJW`Gse^e+c%GtnziL_(_Dk(2=9HmR$9u4rP=`$Gzs{82qCaOrT7oxjkkV%@$N;yOgC?5K)kfSDkB zIf=<0omvjSi(PWo2{62SHug5=(_U*W=2eiPJ3{pk?qr@t9-+yC2?enJH36-B&7dfQ zhyjycOob2G3K}RfHyqV3kwA@}ws;dm5qSH3=nqj>Mwg%7gB5}7r5>VB_kf76%Cdgn z293Omm#fnb3@P~>Bw!G{QGpx}H1UdC-lU?l<*%u?)$lIMfD)Hib4 zLwKvs&$E3`XgVn+AlF zhDpq*cR#i$e*+olGigE*B93UxTDh^C#ip{#V-Y{#U=#YpMf|X#<*^iJEyh0E_3K+f z9i9S#zh!jRVxlt|zrm?@_Y<-eu1)bY{v@0-ItXzUC#|4Wl7#RhOh*s7O{as|0hY-X4KtSssu-7xFmnL}0CwFseZ}{K3bNY3HIrvCTTa6yEE(d)!%4#zw77dlhrt(pS!? z4t`rVKpdi*gDMY_?86m5gIDUsQ~rd}Czg}ZE=eQ(dcoNRdkV-JC@>M7sG2V)0tKUni?JlU}e0aR*-Eh}vAA((^22 z>c`w+O`#kA`@oR@#K|WpC%1TxfxAdM#>6xgyf`%MZK=ZPcmY%3!C#~Oc6;AQP}z3d zSEfEO40axscJ>R^NmqYhB2w$alw(tzQrdflaYIk4b}f@%D{htPVs3n#sYb)f(vQt# zDaALuM=7wQf#>lMyF|EhMf7KpTo2y|Ev9X^r~adG(4%8ze_GOa;~R_2@`fpix_%Q> zRVJp?NkTy;ag#8j~uiq+@H%IcJ`;mhxKoX!Ew?puw{~SGD8eqBtdOB@E7QD$W4Jpa}E6wHD4|d z1TjAy3Wc1MGF;SZl`XJ~A{X?-yKW5L?e}ISA|PSkAu0vVYp_aw z$^y{QVAYP=eqTF|Fm>c)2wYpECm*5#`jl+cGyojVq&YUCFeF!C_mp81yn@DhWGfl! zF6dOCf>O5!VC~^?0lj4N52G{DJ>Q?FYCf1bqM=Rb@q&URTT>gR!8OHgwNhsx8~68! z&T$P-wx&urLPWzsCGU~zkg2xYe6TDyGmo^dW?I_tsKNgPWP#107a*prve<%wW!kwd zG+-q^A(-%%@t|Rldke^RGvc1X))I(n`ADXNv1w4kNUuhJf{qK5gJx2Wd=3;@bl>>j zgo&b+`JRhJtVIQ%Q_NAuiFpyZmu%(*J3R*d`mI|Th#PoW*EYgxQwTAz2G(bUJ#|r2 zz4+V<#R{gd5ML^6n5Rgo1Er+{^D*eJ7C;|%ei(6Acxv0gyhdyli%N3~X;{xiIo?*yyl4l$BJ4)yj$FKek>)b1y5UE5r#h8oupfiYhT z4@Rm1kedy&{{8#+8=T9<`RRZp)bQC0k^%oi(8ah3(v%X=1O6h`<78F9TCCv3{N%8R zJZo^Y@P6^Zum!rxc_r`+L@aadFbM;Aejz(AGzySD2%+PYL%qrmUjc%~C)YK~t}lvN z@i%G!%IBv;l3^3w^A0uG)StXiu$ z`_#5D^&Bai)^5{1e(TeXNqK8}ri+RvDPWl{Ayb?(4rntlbCGZ@A*P9f?&t&_8(auM z{6KR0*bewt(7fZ42YYD(;lLaJ{uv3f(L&=uv;p#>g`D~rZJ=Dgf1znNM8qwfwYct7 z`>%B#8F77|X_0oZ@}M}}Oxrf~RN_?S;#AAA*;e`i&uGf3?Cnbu+*uZGS9ez>!O6Lw z^?<8oNugc(tNsA9t~ASxv{g5sZOR?;$(+t_ezKvtgSWYEfvR7q-&PvT(_w^=Q#NN ziI5cF0RDH?pY!HM53>{Fwl<_p`#js0yIHDM^Iv{sq4iGzX4-fiH+(7l<=jTn7~_9d zSMl3#>aO)HW#RW&Prg$o_D2E@Ackn3_r{a|pToBgoBgMcxE006P(2fws zTN!66$#dJon^V2EI z2lRT77f~@W^-$_maZCUlMmXglA0|C=zzYu3Y2Sn@tPDYNgLbSD_A|i97>~k^%I^ae zi1;{=fynkyp=oEbq?pRir45SpgN;ar`3>C~64s8!a}b+}T^R`vb!80lGhwO8K^0K6 zS^1}kIs;wX>RmTxuOBJ|$IT3}Z9yBy95v(ADY07{=$r91+Vl20LbepWrvo{dL%kq; zHJq`|y@b7)9R2~%GuCxU{ zc@78&9;R*bp##izER3kB*Tort6cHIRc^?Bg$S)U|2mlQN0`A3mMqy#@z#*%2xY+Gm zZ73$5=j-Qp#TU$djF1juTsa+)cJR{?uftscU*^LzD0BRDAVTjeugAy{Ser)%Vqjdy zxh@Uo8|K!qzvcY`;`V_bN*ll!PHQfsT5R~4b;%4VW&?I`sDLJ6JH#hRU}b~Q`PpB* zBW$Dg9gChA!BD^yAm-(WVMButiy8QZ3EtqoE7d(|N|ZSqNRQuO*Fx+@m_u@U9PWlA z#%|UyuqgdGAUY4jFyzMp_b~Ca0b}Vu*0aZ5ar(#u1XJ?u7Q0<-nrB^-mmS>s`-(#1 z%qHYam1Zmy((MoNg7fdXp>MKUh3u6ja~2XpeC6i4Y zEb+q&y2Awypu8I{S7E?K0iJ*p*b`AZYcrXF8kZ0Gbl`p9^vc=M3*9A>Y9LmtIHimo z9S@0#41On1nCzWTN1M_{URCsN8m~0Wn=y*0&V3&%-99S7uDYe%vs0|^_ousoLZ)2X?+8R6 z*}*WnG4@*(v_4BE+9z@Lqx1QN$eF;_8XU2>mD*bePnIm*5iZ{Lz%!~WNR(0V!eCKD zsq`lEa{qoFcFUs`pupWi_E2BaC##z2>L2YLhE18sE97(|kY9l`LnJmr6CyiSP#@sD z5t!&k&@P-XCwpYbcwxT7c6SP1qZy0IyN?+jUuRf09V3-pHQ%}~%h+krgA~ z(xgH@9S%K`q!aJko}Khq+Rix1#xImLM`_-SepbHA-ivXvwTiX1X3bZc6PV53c0I8_ z#?n41?4KqqbU0~Emo&>{#?qv(bXG=HW6~UjWhKGf5RVOKFjH!N{L}(8ep(Z$l z7(Tz*jh(du*g?4e{_xswA@%bKO1dMc2wsseA9Fv$0{Xv4KKk!7ga0#!AA+oAU- zh4$#x*!y;fZSn2+3O~i8Dka|w0VsS3Ep*(g|pVcScvatEG*y)wiG<4L}A2hCe>cB0> zw>+D7^;ha*5!^axs=_dGs$!8&gWB1%<~w}rrLRbM+PweLUg}^;S;m%nB*ISLDmmt< z*=q|86C1;%0k^lmMjlZ;(kAEM8b2_;O<}^(VCoRO3U)>2W+RWDoIWx>IMiRYPbFyj z)n{pXBTrP%^KsgEM0)5%#EM(F=@OfDq8khqecw&xPdX_CaM>Fxot$ZGG*nco4QA3g z@H%(xpN$*mYuzta_IoLPVGBh#hkfQsob2_=s%WNmt<~yhL9%T%E2Jaex`p3BF8&2* z0nbG@I$hn9i#O&9hvh~79i`feVRNlZ8JBWb=&}ZuGtPn&B913cbim3)e4VK8D51ob zgB&@5#(PLS7PT9e{C8mF0gGKx*U;gTGhuMzsmqsKXl+Z_KEaiw>uL2w`snp6rCM>0 zbJFwIseeiQEiqaJ@wt_;Q2y8{8`9=>76(x|Lnxy`frBMO5!@Luv>_{_>z;-hLp+{@ zJ52Y$dA3Bkz~P9%hHwr60gZy_AYu(TGh*V!b8$$PS5(yDSl7IdR@T$=;byp}b`^9z>>dM`NVq`UC?aXX8Qe! zc7lRUHlPsW3d%Y_)&L49B!zvsY4ByPmfwQyjvR#$9AcvNXECZyS4i}H0>t_0FbCyf z90OwD9;Se)fGr0M;|h78@RxXTIP-kR`d(c-2vp^)rYyKf83Zn3 zn+`}#uYvsKXhz;ZaClEW-Z2-no%-};sih$%o(z~DBold~i#c*2r=BMcVIDn<|ti(2x@NpmRS zz+WpETVegg6DAt2(7xIlG)K=v?E}r3NBS|KzBGVSkamH65lTL%S#G1(!SPZc0=0-f z41YNu9B2%VPnnqmPA~sOe$0hcUg)dB?Bqr0cIYK3_mCTF`L+R6IGR0%Jhz zF2tsST@~9?X<%ge=>P#sxp(xyrEQ3(M<9^k0#E{VQxzqhg8l-urabp3U?16|PY$9? z-8y?+lRLOBm?(m0fln3TWQB2Y!zi{-3)7!qG_bkL8ZQq2w5c}@uYEd%PYVxK}_TXr;c$fIV4$W-kG+FC6TK-%AzXZeyu_-g!a__= zUf*NAdP^%Ra=RWqCI_}wXcA6_DRd#4E$k1Z;k?_!kM(}yP@gwr2OEjyybA|5tx@rS ztI~OG*_$_TZL+Jdz-@1otjfkEMmD{|g%~+>zJpwcX>^mC)>(kf(~wGt-lu;^g=V;QBR<4&a$DE?W;vRCXf{-qHs9jHE+IS)AVDfM5X&Foi>-p-5jF zszU-*1fPI!53QU1ZwWk+hA}c1$P<3lq70nJa4q8&hF>4H z8s23^?0{V>=laI2muyi4wShyGpWw_6p^v@>*yM4o3LA#wJ%ijD?p$H_I-fQ@ZP#G= z_So=h{zrkM^@l$+=I`NKd#i0_my%wWawn&c(#7Po375(S*}m9{G+_wx(m16D7Q8Pa zSn%kYXOP9QYlIx_3kH@Pz1CPpXkEA$#V+rdYeZAgi0=79e7H$Rg>r)K35|6yyH_`i zPTQH>I;qacDIJs4X}B@$Q{l5bu)JXiciTZ}7Ml3jJUd|<59%H#U;OufJ>h@%e@R03 zdOZ`9SDb-Zq5+rhO+H@}PQ9eX-PL}>;@C-5Yrp59t?h$6?FYxCw;f=6d+fsVpDIVM zU&WCe&oMcI7yX`if9sm~5wwb8oVs-=eB_(7eQoM?(dX;w25HBFZiape4V*ljsF$Ie zwD-oC-9hmr;}Ndl$2<1-G6LX-+qmag#)W-*>FkZ{-=1&Cx$)x2XWQnYLxm%>rMHi! z9m7IWFa6SU$)8J6dNjN$I6ur;DZ-Y3{iZg3Y`WXZ| zQilG84C90oSD{K_s}V)-!BuUt(TJwPf>dY-BBPUxaUw!;?TS<{EVZsX(s$AMKraSk7zP?{Cv9rNl~Ro>-`qu|z4> zUC5L)NTtj}(WFsHSY=9v$XJp}lA@(VgIZWDEJKlK(n3-xQzbO)&lS)6y!$!c=h*MD z-~H@;9Q%0gKO8sdzJI^#cU|Z3J-_Gogc>@y*Z0d0H7^5oE$nU~QFKCWaezjxo@s}N-; zD*1}24_|NqdJUB8CHI5dEa%BHlVWoO0!we=iz3l>3koM9zSwWeaYp^ejLjvk6kLUB zCQIL+K1LeTCec?a#Dp=z#UH<2+G+K<_uI#zHV_*dq$u#2+iG#)9(gl_?g&hndK%kL z;qLv+zSq0UE3L~GiYpS3G+{j*1P<@NU;i}gxtm4Bj=tL~z|c2Db-^kzo zvQtqsdQk}eK{|ovEwHsiA5D2;po4}ow3Vt!+pxZz(i3MV%PnN(X(IP$;}W4$N}sy4 z6&RHuJ`MDpSaJ~aMy9H^m3N=?J5aw0ZoTNq>P55xyqm@l5jq2_;q0LDBT^T3B^8y6@Zi4?edR1##kl zw=Dt)MSE9lV4A}b^u9nq#d_AP-TUvX{a?^gHDiB?3P7mp$SvR4_uR?Rv;`HKUmDNr zsOky-Du;iqakkDA{?%hssMYDO3*uV?R_9i9*q*fR{NOe%CCSwJ_l|cOY2NVi*|@v5 z4HZGE@0;aJd&dS8tc&b;J0tYUp38IFEA@hN)V8Jm>-CWTw=;=EZ;o-Jr}=`k@oS3} z$Ga#LUEKP|ABw?2K&DPpXU&{xJfln4h;3)`r6GNzydq3po|#^l|4WgGWjFm|BAeN{ z)NfqoXPX71Cof+yRCozT$JHex!#$m4Rtf4XFV*O?GRd85v-SS?YSJ-$wzc(=Qtv-o zGWM6Wl{^pF{VSOGr;MeVHOH$%h7)mGhazvmxS z&~7=4F^G~AZ`9{@%e%r6#t_`4U;c4o5h@VEFECn1NXz2JEO|EN}c#wv(D$Vd=)#jB!xM6;H`Q~9V{t@oa{ZpD zG#p;GoAQaE0`%}}J5{O|ZqJczB5c?ByMog^@!gVjYJ)^=<{y@hLUY>~x>JtKH!v%s zBqRcFbmd7iTuP%wVMyf7Sn+pY>^cDii;xjv!*Lr|gZ#1NZ|x=96nXeb@ST3l_(|N!re97M$3V+KAoPqtI_P~T9j74U z5+vmQ-AMzB$qa8f%ZvYgVwd{d?_JcDkeX<8BVjG3B*q6mV&2QsIp6Nfu3Urr68$dh z2|C@Dg_g8+xlfsB>TEEejXJw&uxbFt_k0EwE{m7$RQIfRL4nc0oTp^0cwajFW1sBb zqdOePl$P9_*=TU&{;sLVyZt47lH51{y5e}Jf7^aVe|^izN%!PTwf@em9}-N)Hv`2b zOH|e142swP4(2T)dZS`UXShm{MT$;RCd@!cy;Uo7qFD8+rq+-l%ZMukhmP1n%|pg? zWzU+p2XI?OGqXTbN(N5pQ_)(!W8nSvhC9%(5c(EEAobwE$Mk&D)`k+S8x>_SRs)WT zZO#r_Y~Rx3N}b=HkMYqlPiJx zsJg!uUDAqD%JJQxF%zOftzHdhm&jEx;)Mt?`G1A(2((sl>w@P^+r7*ZcyA&o>yh5s z_{_-?spyI(d|t^^Ga8Y|c$p}Z%YG3QZ2$qp-GeLCAm6`ct!1JxfB>p=dU5%&r4y!n=vkwD^ypC={wI)4 z#aSUIC-W!NX4S0PcA;lv(jF|(@Iu6JtD3ct#Ib3sIw&t?C7Awui?a$THHn-lra*^%8r^~fZT@GtZeYCK<_ZWE# z)1Nx72=m|9oHMcF)6M3u$46~eQCat{w!Z1wwuPTOpA}RX7^G+Vdq-4ntkG5p3<;T2 zR4v(*>FJ{ER86d?EJcdZywGO2re@7OD|wHeYNH=qW1R;#k2(mU$zzg@we>{TvMDM? z#>U&HMUP4G^Pw4#N4h^UGTWol4BM4Gr{tJ4*2UzH2)#7BR@O)=GlqUUJzFhFojgB+ z7Z#*=<;oS^h@=uVnkp5gZ)oVh_gG%yZ}@3t?!~cV$7*P3golUk3=Yo9%rxGdTX239 zS0P}F!0o}oUukN~dw)7Q`UMP%RZdR+*kb%tpTaecj-{Z8z;_D0M^A||l~IXV)UHe? z*3bMkO!=m!CP$BD*iw}`MZhDsFJU5E-fXcSe^v}2lF@Ar+=ksRTyeGjsmma9oD5-dGPQP!y}+BoGTa+ zcKiJK^Csz2jf`YeOd<`OR>OSV{^uv(tkQQQBu!o(ezMI&^L8tu%upHCzTJtpRZrDY zqN0|7DhtdEtTWbSN{3CEC9gDW7B0+Oeyc~=%z9Fx(#wN47cF;u(_Uh&Hgcr0wDId# zTU%T2yg6Tv8CFp`tb3!B4kpa;pD=M^Gc5vdF86JBVo!RrTFRL-0uSsVefjFu@rmv? ze*JX`!Wx1{FhH%B3k$X4gE)xHTHFKlt>+3ORVH?j`sH2uu zjBCuYzkf$v(xhB`Pvhv}!bqQ4Q%wye!#r#~OER=F;jZMJtt(#@AMao)UJvNN6q7>e!=(jWr1hdBYTVEc zQwH%>bqMZd(4`A;cnmBYy@f+uGmJNHKK`Rqk4J@tzRd42NuLqmZZvIL!_0XSSMN(a zoKvS(-aeoPsxH)$o^g+QWjclYv}Ep*g-rL} zy&EGV_g^tihVD&Cp$Ybq_oWRdW{K~6SzSG%+5azDhfB+nZw{1GP&hc>Z278HgLFNN zWySzjE_);+@qgrG5)~2>LV7#_ty|m*#fNLt4oWtfr|Sm+TsFX|xOM;jTc0`Ix^+`A zTEG5tghqeKm%!S)C-d|3<>lq2+l8mP6}f+WUm<=dxE;{A8){1=I;%cVXSw!7Tk^}& zrAvQdTVj*He*PS^ZQHDnB}Nq0`U1sfWSke)sMj zM3x5U8p-1297r~Lz^9C71K{_9r%;wl-LYfGzFLW7*D34?*(9cB4o?%F&Bcr|Twnw0 zU$hwGeCnzqB%8)~EK{~rRZtpGlHEpC@)LRBJLq-#EWFcEWIuhGVNit zWrrz|53y4p^hIC1*4s-NFi4pW~mf;uw5ie8U&ogf5LX8Y<+-q>_0EzSGp z9f2WnC)w9hJ8atL=^`typwI$%Zck!>0IeX8hiu<&%IHzHu!DncM6FOhB+hkt)x+}yI}>ELCWYzKZg^`N@`MpC=I znw6cRIdT{}=J$2&+EqhcedmrHxz+V#N&R#^PAAKC+;k;aO-YiT=x2V~B<{}b+h-CI zPdyR7-!BL0W<{O`Y=9b(4?NH*yu zu_ZDyGt<)xb93h<&OdheaPN$h#}eY>VI+#YwYj+&ec-CfN{2T)Te2awuUNj^$Ip+< z`39EgNjV?Vq#KhXK5J5C{TG@A2s`XEeUkArCr_SCPM#NQHc?NH=;-?O>w#Oh78Vxv z&G;iS{@l5_OkX2t2Hx`Du;Gh|>;@bOa*_ygXib_FE*7cS(|GgObfhE$D2XN~C&S!& z+ng&D9q6Fh8Wfa;Dq7ZLUA{X#F*P;y(j}&NIS^nFrJPPo)W#^CKffAFzILsLn;YuJ z(BQvNmo6zT_Pu-&THwp_^2?Vl1qKD-a?aI6VRt@!_<$S&*`c4BSXY;loE)E&bnng`4w{f~a(8#v?t2%-tGqk? zj39%gQJOSdzFf?|eKb<2nCC=Hk=?L{&*H$%j5UigB?z^(E&4iCoU@2ovu4?%K!Nzs zf(31pBM{so8rjiWGHj{P^P)jv{f8I~(b~0Vv$C4!M0&p6`{m1*GiT25NFG0aY~N2w zA}LGTrF1C)H=?)XYR6?(;)9Ein#7GzRh{5n8$L7E7w+uzkiO+*Wq%o9rmDn>iH0SN z<;am&$ew_U2`wElOOHbN-M^mob>^0qmKGKk=H@S6zwX_qPi0xz2@7@k={;qG_(u?b^>Dp(<$E%GR^_I?{ypW=zqHTeJg3<9Q&4fqx+Bzmj|5SRqFQGYW zjD0uYL#5KIR46nwHLub9zP`L?s!d5!_O$4Uge`C%_Vk~4a(=?HWy^$0j(f#DKoNB< ztvUqL)>K>*Lx34>j5lGYp#(%yYfCI$w2I2^1)e})VBpoOQWuxEf*nZ5`g8-)rCv8y zPEL;fm1Tb}-w{-^xTIvxoTAB}sh%4r&eze=DJm?y7_w-Dx;ji&QFvjiT3J{?qQIxH zi8v+2^d=Ds%5~}DP?Q}N^(Wj9l42r85{US-XSKAoJy{%g_m3Yx3RTml=H|q+XJba7 zzcGXX9T)?=)QJ;|Kh>7xMS-HP_w(}(ZNDb95UW7{bj+z!%Ds9$yX0mV_wZ>^k)EF3 zwr$(y0tGP~ZmXZh$dM+qXA5jSF){IBUfxFDVhgy!M+F63y1>YoGetDDefMr>JtHS4 zCnhbU+~f8B`P9lA%#`KGBV+_Sa!)~ieo4b1$Ss?4_qP9Zc~V0G10`Vj4dcwvw#J8 zdt`$KPn@_O0E6Ru4c(v>jq1X?*I@L(0tg*9bPJfx1G@Cl zLOqPQ*d{UQ?Aazn?2a8hdiCm6a=*frzm9Kv)tDe3GzmcgzUgRqc>mtLCoKP6k(COW zW^QWgN^G`e%N7Cz;x3VanFWMQ+aDgDNp+s}y>#hP$BrG1rcX~xNm1%0CJzkI_qB+( zFrfL2W23CB%y-P2HxD_!$#B`(?0og$!GmM+yO@}mM68`%ObAVYLX0Vv$Hss22SWKV`Eou?{2cP>%6^tNs02AoHX-XLB-3LtF?{}Y&Hj&e67!UI(y~HMiOnI z#Dnd{HLa&dfy7oc;?s;tbgSjzN5$W}rZ9GX0-=!sjc2iDmmv)`Ha7A|>JJ(UT|!Kv zr>AE#v>B34EGcvth6=R$<3VzBbHxPf^m+Gz2qX6HEh{TK&m_esPZp4?as+SQyeX*Q z`i+0`@J@KEhTVofO{&lUvbzW=pgD8y;@Ws1;Smu|YuEPg-+%4ewYnoS^Bq$~3eC?@ zYC|EeiDDv)_qe1aD=BFt`LK%05?aw<+<3aVF-Umtfi!!nQDW|jYuP57#N!NC;pBDb z(18sy6810mBnp_68~4~GUc7MOBPn2Aofxp?sapc21s`N6m5P9rL)S$GJVDo5hKG5S zVQ}?TmZ>)SWYTmBJi2*vdfY=hkaCJ^5Vt0=v0*fIXy3l@MseSeR~S1?jUUaZ5Pjn2 zCsbEe?Pl~37$>9|+B0^;9zk5ftb%*yF$fL_wpg%hfTyH}hM0P#p#s7q*1(fjw4P?+ zd^@SRT8R#P0k1t7IB3vY!i>h`)KpK*C4p_;9#N0QIT#um?hg&^+pk~j+YB;J!QCFK zqchjoxc8W)fsfopw{7Qc<0ozPk|yMjWN-`F6s;X$HXsDaY*2J_`xD^W$rgwCVxpe; zypRu)CSxs`&OJ;2JNaEG;b*@xWRp!<)P2B!W}!E)tjwPB;_A9|=^|9Jv7++w15hDf zu;4IhGkzE^^5=o{bdmjYxF_i93ORd-{-G%rkDoqW4**8r-`w@<*TV}UXd%y!k>5i3 zU_CWmjOCLjPoT#=-TN~A@?}h-m6cThI5Q}sOxLb;_4StOh=L!K%9xv*2T7NZ&N(>5 zo5iQE(|h&m6?w;}PoMVe-aUTO#+%o#lULDZ&`f0=1GNSV=zTCK=r?u%CuNBKCywmB zquSMEM=KEe{Ovb*Ut4d!1v5lMWKrBh)J`04g$~`CdJD58*R9(q4GmFRC#Lw+D%fkg zb`lm)hjx3|AZSBQ=8ILXQ&}!*nSh0~vMK;MeDh|l!w*|o%y}|n@NtG6HM(@`CWZlt zZo_*cE?0VGi4=>6fgjZ!n7%@vUZ4H;=NMLysnY%+Q<O$R{)jQ(31}iGhm#;=)!o?SGkyB> zYx($ZFkIOD`qB7|qpo|Tqf*{?5hsV@`E%98r( zdb~zj-}&j?;^NE~ewu};cr`f{5l6oFVEt?rq8?V3QfUEoea^q?o6Yt0O;;SOs(SiG zC81(ke^KS~-!F|Ct)ue^#Df=yY1b}-GR+YqMrdeEqL5%_mIo42=bIH7DdweVYG}}u z3m8jevth#q=c7KHs=j#(B227!V`;f!>C)$~U$1s?33!-`J}P&-h<~RecYLSE)%Dp` z_I1Ag>(+h1=v=sPfv_K#mY-_%ox^FJH*cT)xwk$)am-y@o{h}wDLqV`|M2109=eJ1 zN2FS?lJnQApYZtdrvxW^r-Kh zhhoq-&a0{_9Ud;yL0GGa6DDA>8U2##SKr${p!oe>r3K{UWzGP|ylQG{Gy@+vrs4pB zMc{R!_s7iH0pnKg3JRLKW=+EU1i&1g`T(mQDbWSh1kN#UPjaD6dAimP^R`5-`h>H_ zt}xPt*!(l(1O){Jt`j>(0Nl{f&=$Qnof@tlcH6deyP`Dxy@|8Ym)YrA(Ve~>i+(?O zc%&!}CIE}hP4OSF%_OcU!`r2z;^2V;%r}}7W!fLwHfM2b!?9YK;;DHiy@-YfbSp{e zE*|MZe1*S%*8GH2j$U%I!=XTMPe4DD!=qK${zHfUc0M|Z_20jLf6A!9B@u5%%~KC! z>?83Hc7u2TSZ=_f+m5M)oWX+!m4^=>Ug@;11ne6WRD^UOoON$8f?wb6!P<1~nv#}g z6>ovt6@xu~|NV98Z$r6DKF&Am3illq74>3R4|Wx2mf$jN9EIgg-QNdX2w4pO38%v3 zA;AePyx2DM!R@(GMFn9Jk2)Zr^zy+7S&TC7BeOL=hp>*4B~+p*O{4)HE#EE6u|Xefi=W zVG?JmN?<}dV5x42V;};|sIK-%CoscLbN8eMB02fl{#-$&M_OjDkZJB5cT(p^j~_!K z_QraA$3M5p_ru`emmYiBP5rdM);0r3;ag&v<(q2PKIvg4wvXgV&skqssI zjY_zEteHOn3rU<1y%J;?h5i%Xp}h!<;8szg&}Xbsyv0gF)(NgT4=xcsit2Asec-lj zd-`gh$;{k9?8}2QTV`l@4JefyJ{_qYiKO=|{sC)EJs}-nn4t7ngPN-9@AvPgUAPd8 z;~Y2c2Pw~z^=knYo8bOxw!OW5(F1ee=0d3E&!0bME{ekf{jwJ?Uc7vH;Mg%gvJtO> zbA;g=8eQ3BHi=$tZVt!t)sG)#=s*rc}_wi{IG^@DD&GG$K7|&+6F>a^4+_S0C zZPls@Bs@?g`|at|&wKz0-rI&1^fF^@66DK|$ zJ(G}Fh$s_fvAeKEo$z@j%YMa0;c0+w(1$&EXR|P^{{A>~b#-+vL{3(=>o%dhMg*|C zorewwnHQEol;b%MctJBi%ob*qQU&3eO8_W_WZ9BMWl3jFpT6Ya$Fch~vUfl6vbD2| z2oI+!lKtz?ok_N4>5cYiDcwQU#>dBp{BAr3i(Hr8fVO)%M7>h_F>5Uc2Qtm$8N0og zri}+N?)B?ND%asq(}EF=FLPcb*bg_Bu3ANZ^mzh05=E$Sl~lPohTsS4VP`M`L@hu{ zxLV9gU*?YQ5p!{z9Y-yayl^_NdnqY_Cv2oV&H2ik z(!G0kO0#DY6ZI+6LJ$0*M~~;_<@v?M;W$`y$4L@lSLL4}A%CzVQ9 ztcay-!@#jJHpN4o3rCI}%gD%>Ze&EF?~ul4I^#JgqW z23YD#w-Z6II)@G&>ZPpA4<_pB%Bl<=G9;jKFpK~Cf|Bf1W8>c`{Y06?3x=b6g-O`V z^5@S_n#Gef1P2GR(pj0AVq_B>0wV2x%2mF8eqxpa8-!kv3CQ(>{*$Ps*fQ&L|Rsc`yZo zAwvuk=Wh!NTJPgitkr40dKl{~ru5=Y0VyzL)fk$xGE+OdnyM;Y%n)D387gYJ&W9l_ zqnS0Jmfm0yEZZdV>)<||89E7seu$@Mz0+>||6+}poN1<3Ru$#t+)paG9w*As4@n+q z?tzLx=m{6ZsQ9M7Fx#j1dL5SHq{d)9Zu?vs08SCLCUe3sf)b&&!G9)@P zv-r`Y6b=w`H<7MRJ{Fr!&X}ot_e}t3#+9gX=hUT)C7$X{VWV>`pPD?xV z#n#&TZiHSzVc{3{8vzlQ2mZh&?KbIM4ntu{C1hn~f$m~@-j&L*yB<6+O`Km{Q&ZyR zfpa;3el(~9sW~d+WW`Vn4$LekrDM3EcHa=vw>^9A!@5ED2*aObs!B#)T-$(Etd*60 zakj31ABVu7f)oWJ@7$SDeUn=me+DAPl#tJ`gw55%=;AWKWv^MYX6@QHm`nLR=f!>; zurs?)#+Jy3qiT|8%?MtP@ZfxF>&mLCzKV*ASFQ4X7?kEjxC~o`bG$Qrfx+{&JCjZz5DjLp;ODl zBH-M+SMi{PANCH;G+)fUHyunKb$(2|1=g3B>at!U(B?JeV&ld(ni%hY@AFM$Jv`uL4#*a;%33|M0!(h7Fm3P|R+-X^4cGWkNo-gEMJ2XbspfaIK?*L*>TH-!J!_ zdrVH6V)yRzY;1gy%ig^W$q0MiQ)6bIdu>w~WU$c2MwFJ;tSK6?;`>I)xx0@q1vLs( zB>HhKIF8ZZw+~V5N{_Ff-nw{ORE>)G>)K2+lHDLjvN2vqCws%^bzFW?0|8bSm4$Y8 z^4Tuc6nmuGk2x!U-#U`49ZHSLXA={r@9M46qVq)iNrv}1MR2{IGh=_TlU-~9{hvhVI!dxFRpoAwud`m0ddt}09X|g=KgM)!GdPr5nW=5C{ z4|H{P#VdRzbm`jR$kC&Op+}GQ1tKY->@uhDNmR|_9OA_IGiUnhd7TXlv!t5T z`1zEg+1zVhiV%rl`%_mI&1~TGK)j&tSD24PzJXkeX+pl&*g7IuXpx$@IpPb9$gA>l zf9Myu9coEyGsa(zja>nti>U)laAxe8Y&Wtm%{6Sc_~k?>=T_w!yo0 zQQy1l`lod2)SyR$ojQ+Ul+m?wX2CHf34g+H04EXsd`ZyK*m~Gc{576^Uv%^`))`=K zREVv;eG9rE);2a9>0BUQ0e*dt#~?AILv&oow2!EpN7Zb{x#GiUa0sWIp*Z+2e_2dA z1X79D%FA2!ecxx#Vc(yEf;0OtX(OWo)l-c}1jJhA5W5M0ecelAbMyU*CkYA1je7_V zKr(B#YS5raNV%eY5!78mqly0}nF^f8uPucm~+XAjF9soLmYzgDRg@d=7Q8#=hgKy~mG_jLl)o z-~dGeitFW1=3lffj56g=zCuK3XYjat!HwUaNnQ7t9J{fLb2fqG^LX;Bt1Z{I3@t*q=FV$2u)Uo4zXR4?7mG?htw zXN^K-y>F~ppWeM!uXgkFEXiE&j^zM5=fS6AjKp|d&?+K-G=*2}+0e$^<*I6@aYaI_ zk1`46leN%YiPAwAwEF*&no4=b1H2bbI4|%J;EF$Ab8@Pxt6w~S4$JgG!xsK~#Gc** z251JldU-jg{c`CCAa`saFVU1OdX>1~mQMk#-q`Kn;8|90zkA?KneR%^CxxDTBJ`F> zobsMOyEr{oelUip-q%0l!F=CV+oIHKQkXUFBp&ysA22Ipw_dX?>l(U$KjZxtSy~Ql z2BBI{M*RHwQp3(**C_%2!x>x4S$aEC!XQZQkuAsl5)Vt>uu?P zAAT6eM#a1trh&avPXdGoDTuN4{%#wlD6qY57Z&Oc8#YZQ%2ZWw9&29%;Zo%dR0N+f z)A<<=EA*y8`o53u-gUyb2u%IM2WlqA#w?pwF$9PTPVTA)I33i39$&xy5-9?mF=Hx|=YC(1oRY0djaGXv z`flI3Gqkkm@$QuVJ`4}486Qmb(!jTFwULq0)TzJ0q(WLroBWxbpHiaNc!(p?Lesw( zjlFnr8Pf=m9zdj68}VwEV1}QmG3m>_$jPl(GAeUGff$sU(kwl4Y4}BmC8hZleLoMi zD~XYweT`O_vAepN7&mU4lh*A^p)4Xb>MxCre3#dYI_?OJ3-p)AYm(lxS1&6|OY-)^ zM?X}7VYj{h7J#jJBV^*+UhAd_6#Ae0_B*ZV``5U}{Vy3d|2gmGU;o;+g8zV(^sm+V ezqUGWTH7^MnQpn)n0Z5d=ZvW)MoH4eTmA=ucO(*PUYC37k z%Ly6V*{~a$*cq9!yV}^}-DG4U;;#0F#^+6)n2b!#Ep0_fL$8WROqM31Bn>`!PI-F? zQwvKOcL!4ycLh~r_w&YrCM0n&CJ|R5T)@WE$&ks_#@g0V$W@fIbX_5QP5hXH#I*Dl zr}Ltu6T}BHY095vlCX0yW#VV&WHaXEFMX z$1TLg!?f}u;nN&U%!HIBPpy0wz7r)`I62u1ad5b}xUjqMu-iG9b8ra?3KG}g=4Qh? z*c{z#oeW*sY#sOixq_srqp^dfy_2P#EfaA?LnAw9Cs7jabomi%?Ek*3t>eltVZbfeuyJv# za&Zgsa0>D8YcIb`-p<6*%>O+i@r;%>hUTUm z_O|9EroTr{NW#wA&H*2X+u`}=pP!bHIO||%W@(LII4Vn@WI8P+!Otbg&(Frq&b4%t z^72BbZ5^EqZH-M&ONx>(9_*HuCPI8B{M>v-rU*r2P9rv6K|=vHqhltb>>39FMnE$*T8G`uo@6?2E{!VVDw#aA>NL`mtIIxnf3gA91 zc|z4SYM{-wjiUpuTUc|wX&SnBxe z4JS?<_upI_;B+#KnrUpu#skOQxrZ2yyK`zn%gto(G|hXlrycX?t`s!C_+C_aGDgr? znM!IY%i>?Tkp>C^f4&J4yqvr8^*OS&j;ofxE+*U9O{8+Xd{*tKT(|rs*&3$%os7k@=0G>7$D+C3=TdGL%=e`Bim;(|>}cGY$Dm5C@* zXyW3giR8D*bL_e8d-t$fvRaBdL-LQtR2_2aqngjoov4Ws?WvB|$#=b=Sbs-!cIESn zSHEr@;B+7RDRE<)pA6oxIQ4ydLIqd2+%E1GcOBWrD3*_p?d&|4 z(4SGN_vZ>svaVzi!FLZSeM`INF**3Iq-3D!MNeu@`-c#gz=+Yg=~2&_vGVJJyeso< zvi)*y`vcAAM)HyT(SnZM&y7n@o;+D!a%+!(ZF458`JVO5Z!Ff^*zJCgj?bzxoJZQj zvT)1Fk3Z&{SGPWX{J5;F?3K%q*w{yjhYuezhp?tZE{@iSlXlV(ufCqJADk}M)&uVX zXuf~{enc%(^QGW4AhC__byUZ3KmzQ4cau{b}; z*7zhoUN6t-w4T@R&ezRZ=RL;z?%lm>63iwls#jsw&N*6g_b%0j3GRwBVeF}Ve5|aj zOr74hGbn`zOA22)SghQNCXYSmWR3XZKtsysD;u`ghb=5D{HO@qzH#F*m%*seo*Rr_ zFE34x|`{HQQ5v@tO;Y00*!+7L1`JA32WwSxx^=;k@G3JPjT*4EZOk&je=EEAm5 z;nkwQXRz&+fKkcDewj4i`*Kf=-uWk0T)1$-($bQ&bH|ReiD&nXo{9c4EEg^6p{{pG zE5qck)$1xX1arP?wb5v7+q!alkvD~^-XNZ4#`_bllgl&Lc?=b}VDK@_QKB9bAMPE& zSJus$NfC~9PvjZBrXIfQPR}3lr~bHC$m!?%TYHAu3p~btetKbE(_ZAg*q>h1XcR`t zEceoF^htr4Tm)|~mKB0%=J(s}IW{fSjKoXav0IL!JXYLW?z;PUU($3-jvb@%lQZ`Y z+j?q9N-C?U1kwvXVb=*8YQ1f4X`n0eXFXVRTX9Zieb#@4g_}3`Nt#hUoErlq>8X`% z-9)4BYKW0j6q|k=!EOBhrb>c>Ws82UgK1lybKl5)F5~X%=-H|7fiy=o&&rpFu+$vE zbIeb6q;Q0UgxFNeJjlB=?VzdoB~9-Y9_uqE1ObiLorAJ@&IT{K9|el}`}-@0c}xsg z7Jj%Toc8wbg-{%09c{KxNT}I_J6FFNp&!Pk^Mrjnqu5-uOZ)Tc5DtSv#D4u#txRh09?OS8u*4!8S1&bDD`jHF-H3Iev+bT}% zUAboQ2GjGL0>_TImfohn-Q$f+uaTx}8SS+&=jQynre^5(*SA6kTZ_C6bckH9P{PT@ zHH@Y3+-z?+SGH9{a(H*S8JkXal!%+d?8GnnN(52McfY@LR)*mH{-Gg8YU*y+g*d4I zZ5FI@DQW4HiUh@2+m8?ax)R^^^1@rvj}kx1SIAjJTo-sOPWLcMDebk>x&3=`GG`kj z%fW+s`L0TOUW2(kUNgN3auvfLZ@>CG{X9FDZ%IAani~=t+TD7+*+EqmY35O;v@@-g zz5VYbm4u@^>uPI}GY$DXe|)$X92~ssuk1N zq{O`)92^QSO-nx3FL1XyBPUn+Jy6{1Tgj&J_;QnE)QGAGJ_!klfC#r=S)ads`zG>& zaCAhSX{~3;AhVSbfN4u+Zo@#PAA2r8f#xTS* z?0g~@9=DD+)?+Z&vhImmnr@R&OLtYIUbrZ~u;~0)9h-LM)a+1^lu2)Gyj}kAsSsxQ zr!U;xOZCM6EY*SGir)3d9$7bK^u2i~aU&zoMq9fX1+L-8ZF*sL{roRx$5n3f2lq=e zdOl`9fXXcT1Y;0YBeta0F?_PduE4x8qex5z1sjJIGAk>=u=ux6h_KoB^_7u)f z=!|9!HeZPf??jAL z3lAldpEz-%S(QvS^k_J*<(=uE20Z&B-V`W0p{c5>in`F+&#I-dzGD6_9$A;d^2@WYH)(n5f7I94f7bU%Mde2v_J4XL{r37+ zq`5D+A`o5*ZQcl^26m-|NQlT?nVCjPEhQ7oAH7O<6$_Ylf{N1^9X zN6}(mO1ju+CC{LhO^$6_rqXjB^Xe^Ix7yS`KK;V1inM=!o7%NxgjRadLSEipN=hD^ zX5P^(+ooZ;$#;Lxc`?JI^^Q_qKv&usCa2GxdoVMCm5c;C|0}2C&D*!YIt?YO*KNLp z%=qZkE$)tI2z68<4wWZ2XSjug+)+2g=YGHa`t}AEtK?%u7vMwgp2+45Q`ym8aw8+7 z3EZp8kiKH&F5!yQ~jUQ z17wnv<58s_gz$)p&cC@vPErO!L%lXhvRN4vhp5v^cdb*b5m2$0M#{5oTApe&r0PuG z5@>fFen;~nm1}S(BO|dQ)3vickP5V3m?6_0RZG6R{~Az0=7sO?F@t>-K>b&*Ul+>w z+SiZ*JkL%l!lHUnBbE|E;ELC;Uz514>JvGN=4ylAtR$u`h9=&s%W;PgC$4=-37Ims z0Jx~_^gDOP6^ek%biTeaJKi65_wHR$d4~X8^04dhg+HRJCW56Xs$Om~Ge32J`X1*94%zA%NiNp)S>F6MjdR$GC`B-SXZ&@8UuUj|+U z(C|n;=3EWxgwxND`^3eIw$TfjNWWTnzTIapu5}c!{=(@oVNJ1V+csjl5@NlyAD&!i zV}m6|+<;w2A(ESa8NhSlba$k+#l3#7*$vEy5rREZH+P60NyUq8YO)Wd*Knldd_BAY-aiRnV2 z3n)Wrv2D-qOdAhq{I>|5xC>fL|&tXfll4V<9)$O)yp`y{R( zICv1HR*OD2Y7f4QkB>K%kP@>KiqE!HQx6lCrSaye z)(gO-gCuY&wKYwGQc8F6HnH(~mH0FFHsiHr%Tvwg-<}!1y}eh^DB2Bk+b5ijq!}3x z2A70e3}I1hRNi|jX1!d+hmSVo~lIz}vyAP8CuUM-Kh&+uB;yvv|ZDdhW4 z-7MoB#dX`kSjNW247}%^r+$AW)#kg68kgQa$jr>Homtj>bS>7cx`e;V6S+@2K|mTG zNe3dztyod}HNhGXF9d+=iSek;zY3~6KRX#9gE8vD0#JxfPOW9!JJaUU(fhQ(;fH_T z*;f~BB58qe`E6T|QP2X60EDHf+)WU5{e7<3XBC!hy)yXA{{8!lW(Qw^2`I*jc_PDi zPa7K>H@H;lM+u3DhzJSgsi$gd`|r0Kfo>u0&?RH@)K*!!w$Kx3rX>eO)D9^hbk(J% zeMNkD-!*)05D_f-*rfc<1hXb5&CZ>*nU-~J$YpJLe4?UlH8trd2%_#|nwV1jedwJ# zcUTk|0^~#4beIWgWbn)LM-e+27}_BtbaizhMnOLcFjgS`9T*2xQ+MDw8iPy24{tD7 zHD$y*|EdUOYk6)g4FcDUw1tR9q5$;}F8JPjU<(+ep{6_u|lEZalk(-ADxA`Ssfw zj*<{KG5j{#V4sg3J<73e;s(_M``ogdvolzlAxXa=Pp5fh_3aKWy^{Mm-GC-o$hmK7 zT97wh0`TtQPARAbcEHSQcL>zT>Tfuatx-hnrlan;T znVFfy!ee2Xot<4Aso+>*k1voQ?Fz=#L3r^j&4`%azkj11YpE7sf{+*7%kLaSS$4dT z4(=4EF#GX|{GP~1wx2=pK!E+rFoa3~)HErM??5r?Wo#jNEG*0p6&D}pD}XMS8T3x^)F3MG64ofau`z3%E@^yOnV` zcQz)PM?g$$0c)6~44kt;;FCl9%L@{EKfgRT#%vxm-jSBecfK)kyTBe~wulTo!BXY} zYRi;KBTy6;2z4;f%{+KvqnbwISwpOfG-(H5V1%Q`;=;U-j}Jb#SxDm2r8y88$u4Au zpTOV&4N2~kgEqCh_U^TId}~)YeM<83l`9%gH6Kbjs2Lg>c6cuq_#QZLAdx)n#U!&` z>q{j`%=ME~4t$VXL>;>;{U}-5UB7S5`NJAG57n%9%+fj^K*jp4lUZBAM#tOR+qqEH z`N_J`N-NhK(8cZIbI&5;rFDT*)fwb*9#34T;Jk`dX&lo z$~uAyv@00HqMoc;K2E2$U;6f?u3%{=?vm2dqLB|rsQtjt)M+v;US>f!as_h(U{NyX zy7)YLqP+{u(59nMl%GEvxf3<}(nOOvqvx2C$0NU$ddb+;bj}vfhOD`kPU8zf`S4yRXIWTEY**npB@2KC8D@NTN2S;!39CHZWm3`e4Pecr?CK0cEJoR zprv?qBa`hF_tI_qV6@S^$ZNjcYu3j3$u)&&A*Apjlv}D`C|IvWbO0w2rMe)rsEm0Y z8e?-a{poLSY^MpvFg^u9)PJBJeLz4lMg(j?we)wyEBmjP^FF3%aAaLYWu!TxUg=oH z4)B;~KQ}dutleDsh=C%c+(b-WJN}S=m3*d;hdM-F%MsK#7R5r$DmZf^iW+jk)vH&5 z$&P^|Kt6L$(_Bt7#pJG+Oa^&sL>{pxQoie}`xZ$6Ld{($LGiLQ<-3CjrKF^!q_eXV zmx(Y}iT$<oppEP1FuRM6;Th)ay>jdFQ#-XComZ6jZ28AqQ*kZPK;2wpLcYgZO#JyG?;eK7kCP zsIUx3&j7KHj{mh+C}4TRuv9@5-i1_pK#lC9eQH$oQ-a}t53F{RZpqOZ`u01E|V zD0(W+xSd`%lKG3~o0*xse3j7xcKQJnO_(n?XieQUv5d5| zw3L)@_kUV`*3K6q43&K0avh$Ndf>*;h*94rq3%cCSjUIJ@FRT_uCFt>Z)Ov;y(1Y8I;tN>DYc_1teQtDZn$TuM z!RYF}c{z4b*L!m4u0vM7;Q0a8Uj|~E^l9V4D zUlmw=><_JRV2Cw4AxS4-D`NVAiUN6R0Fnc`2goSyvlBKHegOe3czQG&7(52J#@pv! zU)j*ISLp}|Yy^cO-FFKu_W>@h=g@4d)_&9tCP^)f#xDBGV3q;42q8G*K9Iil($k}x(~A_J zfr8ujtQ9I=JQiEGj_AY}W719vis2GUMf9#Xk)RGA?!&sXXni@1ksZY{j^`H{Dcj%o zFba!&YWRELUcoytG3+D^`cK^cj9fmVNNY>;N*D2xKO#OELe^QRy`W-Go;*p@%>`t+ zN7}i2H)@kbjLo+aKdUdFV-Rr!`@xsrKAA@#<@s1Gb()C_tpSnfI z{wvVeH-EC-1FJ_VEcFQYXFK4umlH; znpTJ4O6u`2DJ|1&`1nv_``%-m79W{@4Qpv1_}tM^WL^`4Wf&{uoV@U}0z?P$x{Bo7 z+#E>JIgS>@4B%l0ut zM3#jrfesVrOpcY+X^NJmiQ_#kE-r+4z|EV4xGp0jb8Yi3G&_&J@g3Z`XU`t2=skOg zybloW5L;LXHUmtikG2o;Fc!(iO`9&5q!1;s$g4m*%j)2nyY{j?Se*(nB3dBYAe~dw zrX52oO@QOJp=i%(90K^PYRiMtlhm2fF38XxQ*yKImSShp! z-EVfR_mM$27$mYrr|TXgW8)6Do@nIy$MNx&et-S-7qNVCF9_@YwD%qnj=xSTDlTG; zi(#Q-;YB8FShvn|v`T=ZaLPzM@+;pE%Tkza<7V~0QwQm*hA4$7lJ!9b&>PWKb^?Hc z791c0!5PdO=`B#xdIb7OA@an`J2Te%k$3On6B8hB_{qG!ybf|iY*f@G(C3m8Ns8W< zoFR0iO&m=UDQ`_4>S=y-=mXR%@2RgFDA0pP&P1DSuIYt2phS`xA*>-oq*DHs)%oYv zv>uT8XaGUzLecPDyI}+Bn6@mskX`^7zCb>JwxvcXXv77`5M;8&<;RCz{e7?*+zAOu zt-G;x&l9h{iNQ8uX694!`DhlR+}Do*-Q=bW$9#A4Xh#Wl7SPGEazefPhf;&8j(@C z(7qWA4d@hP*(}4i>qp1XQuFa4qp$?jL7|*Z1{?y=^#kdbdivJ4ny=k4>U<6AQNs=^ z-!8q z7fFkmdbEDw+xPEPbxY z9Nu<`DBBt^#Cm$&OpD;Etzg8&)1d_n7Oo;oK!J;t<-mxQqaLNROK|PD`gE`28#1!% z*~qLw?G&`A{#9H0+Gd_Ve~uE)uNdg>zcZNI^n*!~5ZnC}w!{V4E+~>T(qpQoHD5XR zQ`6BUn>rQ@GkkOMKxGpk2Iw86HEY``^cE0H#6M^V*6-}cQz^JdB1|BlM)pkpnT@9; z%tmC=_OhK69*S93tuLSFJ#+3?A030>>MMh4|3bA5l%5MsWg^&to3+0Y;imiYLEUw5 zV<4`8GGI2ekaX?xZyfs^gQ)`X@RLDlc%iC?@CUw~L)nA%rGQ|Kd7txRmxkU%Z7D~H z5IQW0kZU{)n~)(Qdg@#A-BcvulR%V19Y6(k19>VgE=IB+L)pa8_dQS|2a3X7J#Si1 zp=5r&!Sa-Y!wBLx>+GYtTa!C_nxSN{JMTMsR8A7%&{G*s$-v+RQUIu}o}~OEwE6D^cOU6cq%TLLJJFjs3NU;-rMcnl)QJ6PH!=u0 zSwL+-9-9GXC8s*n>B>_wwUSD--tm%?<%X{J|CZukTU!_C}>Bt zHP9MBOChX2p>YcLkC4#eg4%rnIXF{oJA=qiSWeJD1T+$cy@kz8IFhFjvgrH^S=@n;nM&$iP)B29~Q{gE|Q6TO-qsV7jBCthsXgP-xNTxPn{7A`CmCp&)Nv``E9XXv5V7 zKSpi6gcgZt{S?GsaKXcM)Fu4h|W<78E)Sdrf=57GWG8)wrgpSP+Qh*rrHq34a-| zIwaN}Bvzsyo!p8@l#-PEa?}m$z3(IxnM}!FRdR}o8Xb(cT>fO%@YQ7cztHMc7as&| zwtH>`lGB%r1pY>39y8YGTMa`Q+(Vky2$VGR9Q=t?4_Ts8!v<*@Bo)H~^NRpcfIfiw z`4R>mSZTUbU^`<#)d@Az3tNUFj{S{n<6IV))I9i}-Zg_N{3s8RCe+ZXj=zBC5xS7! zFCzC+N4i$61a2tcZ|4tq5FU!v9OmTw9l%j2BQLK;br7}&ur<_al5(mxUw>i;a%1}S zi3ncH496Y?zPD(J6k>4EYz&Zr`hix)PgfrFK}55P@hLHaj;G{B>^qPDYR+mH!&I)| z696|#X8~mVSMZww1|9NuK%EDklZ?LlTHsH-mpuLwVZVbczZ^*xntzJe)1$aGGz9YO zzn%o{APkdz2!S-!q&x4Jlai9qh`BFd7c;{RMGDO1rPqAkOa|l-e2~Y$^U}R`SK{v27LwmAmxg_R0+5pYGm^u8gl3G2wD+I zjen5J-X6P!y9{K%&%l~MNVqky9)d=~d;mn6kdP1{156jnu4l#L+SAj6R$C6xPqdh) z5I;XZTzgv={Rm_idQ@xV=c7Qgq^-;$NswX?v|kX=u!f*LfPS3hV=%&4^xz@vwSI98 zbVw70X)jvX#kMKq2L>3-i%Lq##Xypd*W#J$@HfC4^CVjR(UnYn>=e@BK9EGg3*SnB z$DlK7;dOo+AgOxWK6H5PMu2AFPI@7 zv?c}}4_hQoK$&{1J+yY~J32aEpM-doje3Cw35rn%bmn8nj`8wlB4c18FXrz^cvo5) zOSl;c4}-n^GZy|_U`zP(2x^733tI~@|ux14)gNXKi7NigvAO8&N&xK2JUk36%K zl0)8BvLJcf#T?vK?3&M*1O=PiZ2tw&?MxxiOE9C>iX6_RvfTWPN1

N%IEpU8O9kquLqlj@=nB9yd)ia$*}2!i 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ⅆ38YOL7sQMn}( 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 PG^gZFlhTe|xcc7!F?}JW literal 0 HcmV?d00001 diff --git a/Resources/images/BarCharts/StackedBarChart.png b/Resources/images/BarCharts/StackedBarChart.png new file mode 100644 index 0000000000000000000000000000000000000000..b565c6772dee62b15a4a8d6de797edae4afc8e5b GIT binary patch literal 31015 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

CrqM9hs^&)p-_Nt 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 literal 0 HcmV?d00001 diff --git a/Resources/images/LineCharts/FilledLineChart.png b/Resources/images/LineCharts/FilledLineChart.png new file mode 100644 index 0000000000000000000000000000000000000000..72682d51240183aa3b19d028d31b0467dd987844 GIT binary patch literal 120648 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�c!J}9Fe^pTD1IYv#18HzX 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
ST*UW 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

*T_no10Tp=BB28 zPfuuW+j;%ZfVt=0A#>W9Z-=@p%7=rB`M%|%4S;*^-T7xP?@slls|*`Bk*s#a3lA^3 z+E))kT;|kOm{5RUnfCkaJ4Kr) zh%o&i+m?M``D|YF3vzCjDN&Jq!9Ygh$BcT2VkSzHM~^U_kNQ*;?3#YxENIp1Kfn51 zLiJTb+K^-G`=0P$rq@S+g+IFp4Qw?F)AGd#{rgRMypMOInum$Gjq@&9l0}Z51TphB zHt-i|t|9b1)tc|K>sJ2!dI=5jE>U{@d|&jHim!-SmgOcwbqz2p6Dng2n8)so{qW&p ztI>r-Z(Zx6#|6f}S;vXRf^oK*_(QcTD4E@M{z+Bk=M~ogikSD^&Z&fDs&V%DGtso$ zcHB|O=K*ZmrHV@8Fys6-6Iru&)&A=+S3EIq6qs6q;%=j#iA^q)C+D`eYUWj}k}AJC z3>ewz7CR0OPLZaSZtoKgqK$rvt>CPhW1Oqfz|SSVYoB**Z}*sUVB-DM>E9Z76#Ym0X@B+xB zF0p*B`J9+Ls|B6;Cd|J|>Nqew}v1s$ZSMojwF_sdQ zHxg9u1nfTeW|z*MTj)%z0an@k*jhXX*xP66EA$Ep+UsKy6rm+v$8PWQEK5EEqxOcH zIF@fs^OR^FkN^ZccgglID3c|&Z^)t_HD+f6kly)lKj<|BYh*;k5xfIj@>^S*gRN~^ zS~$QhyD&*V{YX}=`HhVYkO|UN7;tbxA>RW6j}C}rdCX-!R*G~_i;pc*hs;8!S#0@O zQdt$hi%Bo$RgSGtjF@shzex4b^2%nGW*TTTwN8!j|=Cp zpca-%djkdI5sZaemz`m(18Dl~1l+Z;_Au~Clg@-rk)21TrfJ%2{9?P~&XduFMYi&j zP-n=<$;)EoVt7DAnQe>au5ehY#X^VaEX}t$c_l-ldGFK1#H|yl#`9;t3U5@_akRl; z4dI0!AcX|>dM zarx=KQPo6&uo1Jw^0xkybb{{%Pv^lI<~}fx@>%9(SS5;i4*7u?;8ry;yBdIdt2AM> z$O2(x1GOenCPkI=(j|XnFTDnZ?5mKFy`7!Rv{O!l#zlv6rkOm8?CiM!bqIo0Xz`9Ao1*FJhe(L(_88vQ z$~Dj&f>+{1Tzh_|o7PTFiqp{tc{iR8e-er*DL~3y}Jw)>FDU_-Me>fZEZlp0sbKc?aRntF)ioa zv@RxDUPx=1o1b?A(hj9JS|kE02eALa9G#hjxH{%|3xyMKOp>ff83AkiW#_G)Qw2VQ;IeXPRS##+zCJ|!gu#D&L?A4~Fh{&Ij= z=#!+g)A$o|&orOpGTJtKIDC7{Mdaijy|~k#t3A_vR(am> zQ*tsb8{0aZKO`gs2)Sj*i*kw^e~!TzqWJHoO68xJfg}ER2+XzR6cSY*)ic$Y2yg|3 zg?C&;u4#d!i5CVSNhZaKjO_27@ksE-<76t{UZ1Q%4^Lm8YOB`V>}*~(h@0jD7_ord zqchPji#vCo&t;2h`aWVD@FBr`gKF-Mjkq3 z>sF2U^{+C)vVxdrWK7Sw^mp)u|l z*WtGuf?~_M(Us9c29PsY9o4nu@a@UJ_t$R#+{XK4hmz(OfEf^Qm@^>pL*z*#)X9e84INlbz2>;h++lC$DoyO)=R(B0g@YV?wx>u%LoJl zR1O$ZHl1?S_ll|qy#lu1F-F$_+qGsVDX0H}LJa2s-dx%;@%R3d?% zcufM{fl-AV)2mzMhO1<0X$kQh^y*a`X@+VBypx)mdb%KfI$Z5}2){+K>rEs}U_g_e1;{cA2_gqA%(+KU>mkaH;2;R}z{T%h zePHBrj#8(lr@5R~HE2ev+)HFqxN`FxSJY{?H#dm{k%n_Re}h@#o=NLbUT)PM2`*aX z6&~aj74-z7B>4m8D{53bv%eZdK)0%d08kk7VD|VYJ?tUR!mbE!X6 zNTZ$wkl;ZR-fm^Dc@)c+4O0M55fT^(`ZOJ`)ns+m2Mz88UqW4~4{zVTg_Ic^2dV;G zpZY^{I0Hf~_vYVeA~~bgs4wiW*j3~0o$;h3r^sS(*+GbqFbafd)aVgpc9<{f56%;= zCLk@)n|K2e%h9R8>jwP*9F(?L@7;a`7&47?US3`xlwQT1TUanLG}LexwgKr4M2lcq zNXvjy0@IR{X92*Y_(KB%Hn+Ck`-6T$E?}EfTKbNn(fscaC)pJkkwsF-2a39EKDm4M zg#Kzj16$g>m$$`nt zHqgN2srHq%P@mq@Jt?nm4U3sl7#^EgCEqP3Zk!G#3Jc~L47rn zpMDO~l8cCL89}K)mh0Oyfb$ok?AhmTYJThP?ygj%x~?&C4;~NT_0m?~dB9#QY;5ye zE~~}Hf1U-SYjIcrTUHVx;^yV^;2VOrY#@s+4~t#Y87}^8k?NUrG|`XLl`aHI*7}(a z$6%cmUg{U%JwqB!4hmm^ar-jT@bB3awJa(9u`XXG0)2>hZD4n&Q=A)fW?_LJ%pn<) z{0hy6#zuA3DKHK&>uh;RUqY_9>U)=s_qk^F1LeON{24>=+DzUL+X)LpLqki;EY0i_ z5J>|UQ3ze!tG=khr@giFb8{)FsZmyY)<_rWQKr%&V_;< zD=Qj){y}vewR-du2b1$#YnL^f{2P5r`Lbk@O*8G$-2MSxz`(#zdl8V}I!`PvUelr# zOd&c_SLYKj-x2RVReK@404f!4#Ru>D09;*NU0yQk!8*7{iy<}CgHVdK`FV9^<$Tb% zK>Wgei}&}wMfAb@r2BxDUYJlf2?)k3UBfd*-UmV5{sA-&KTI>5LfC25B+=6el<4W{ zX>diWcL%h9#h<|y6cRdtlE6!;3o7T350o|sP!gef!c_Ba6l+5^g_SA^a5n-5&D;AF zXxY$`=HH7e;tVrmudJ_&xo#Wol|mFl+9^)%2jvG+*EO-k#Ds)u)9w$Dg8_Ji0?Y#h z*0r(=1OxmhOW*|U{`_$6Pjaz%-QX6%0PMbbbDb|RJH8FnTVYr{3kM$V$3y=PDMh}4 z#GBZ2ha(obk0;E(zPJFZj~qz7U0%=Euljqd!+@&uITEz!7#LDPzZ2OXvj?Hq@=9n^ zfpq8GGC)pHb%zaT>Ha>0OV1{eU-}AXL@uZYfF{`4J0~Y=K)1jPtd}QbVqpQf4a`s{ ztOh?nKX?+zYB}hOOsf4oMG{3ujonZqEj|4wFu5eb_WT4fQk~;}?(ZQ*SM4+^RBqou z-;>?6Bn&r91vsNRp?~-+28KB*FXvL%yDA`&zzHE1-`ssq^#Neft;>I`7mUsGwH_M6 z+gn@BZEdt5zEv8xldN|A(Kj(R4eyc-gaADvMadB0;q}1HK701;Uq>GUqYtZI|L<^O ztOn@~{(F-crnA@m|DFpYkNv+LdhK5hfPry%m+arf|E2nW*T>SF!EQyD=UhE;etv#C z*SPq2s6Azd&2K@HET{OVjY*>aAF^f61UduAr93=+K=lxqZ7RRcY^{oN{GP;T z<`!Re^m$3zV{NL5!J4^#9o%aSV5E6@d4RnlwOqmLglhxSq7ZU01!nT($#+WcBmF5~ z&?NQT*NW&tV_xcI5D^(`ZM7IF1O{kUTtNUQr=sau*@YDAHRVglf?12FWM{XfWmCng zUwa;OtijL!Ojn=ey?7j`^S?kgf-8V#!V0JA?d4`-T81L_^s@`}TL59^%c%r|2^bg& zrXTQ02-*qYj$mj={;0zduiR=RMd?YsSHvXdnuSW5lam8tli?s8Ob{A#z;TDHbsWF37v%4E zeLyryKy8G)*xH)8co#r9{Re=#F`ZfZ!j*tkcWuxqf#T_@E-8>4QOD&zfVObyMLWE} zWDtzu(QTlIMLH9NnSp^f|G73**2KsuNfqFuUy?9efIvx2|6X*#^+e4H6PfhB_aa zu`2h$WW(=Yo`Vm9;=#Rq9{fBo8H4Tg;jC#&A1%ejeh22W7&8$&jo=46fM_j>MxZAH zSzs_EUJu5mpJA2Sd3boBP!-M_Xefb0#F2FK;zfdP zu5#t7eAQHzerDO-mY`-%dV+ir*@f}PUlZm|!nf(HX-xRvhi85pmRpS2$mTO^$a($G zSftsINRvhCzl+j!TLSa|v8a{z=G~10BXD;aLP8b_|c=Tl-);H4i%-a~Y3MK0_turJU^U)xD(p zLhW=;(_rD|Z+vpQ__JI}oAkX8*GfPhn{00f zee51+Up#`O*RNBBH@|xI3WPCj9UZxjeBvuip>@g^0VLju4`VPRn{N1Y0;>YSD&PTg@EG9_NUCA|zpq^^48GRoZNll+5; zZ?}nylyf`!7EtzVHLIt})$iI>SLl&W1VR;o6vDRnLKUy#$SxpfKJ8Aqs>MQOt~uIn zg%UBpuz*kw?p(kj?;GD^HMaK$~bp6%-VJ>LO%4 zCQuO)PW|9LLX<*WO_fUy74&(TxVOT`?Y>c$VVXTxpCGCack89`74u;1mmStL3=Hqj zg|>8tb|;aGxY$COh_p;F<{*L+>ACorDz)i?>+B- zHj~F7ANP*S=+z_oQ6R~5DhwAfiEP0ZfZhjF!2OM7+QVy=7qQ7Sn(08grn?ygVsjys zbC@yvIq1S01y^{~HFyIObVlg0YMX|nWIgC8!{a4wg!Zf&c}ajjSNUGYwMMaPQTX?B z{@Nz|xxzH|CPOtBl7hmwx8AMIKk+U-Z@WWff4znLe7=j`y{kH$IoGvU3kuuE@`cVm z*3}G&V6g7#tB^p%<+8R65)haM)pVW;&23pXSm%#aUXwK?)`_HNmsOdl9PvLd@;tNa zbDVV1gNop?%j5@@eucb?z~>?J(fNFR_2cVBpD92D)u8zRE3JR}`M43S|0{}K^{q90 zdZ`t3sovg#y(SeLXe(KEC`Vh?%!r(`B7SCXY_5SOC}P@`1aNt#DHK5=D<>}x z1bV=fClisI0D7}lDP$}#EAozC`ucwMMW67K95Wl+M4tXV2?-3XpP;7X>Q(^;8X#oT zeu5vJu9^X55G@&jQUqRq#z9r(@!lAUIjX;s5(C)f*C)0}r1I?>jlQ76!{BEp_xGg8 ziXUy>n|bS{uzTusxZE_ID)f;tGgLvP;jK5m2gT%bc26QDl;GuA?b*Jc;!RC8sB4BV zn(n=VGfYiQHPS&-4`?+2iI~8Zgw`;*56eJ-$ON9+Ru>ECAykuCTTFb4M?So0!YO>+ zQ7rDS`GujnpzEt4TXyDRcd{lTtRS;X`Wt@Vu2f*a)2C0%oi}p&%`*p&K0@ehc%NE~ zQlo26-x!QCFwAJAf^>>95H?uzRnS1Cowam@7BBF+f3}p*>$;maZx(|uCoCKr0HQ-X zGZ2<}aH_^i&glslL7qP$y(Y`Z>TRSlHM(+ekXM`_;vz5?T?t%q@dA z2^U{&7u~1t9`z%#U%>5@zU_?8U~%J;@?+?M?OxSP*JD~ru>)IkE4bR>qlFMt*EU6z%c>$u22Y~u{|1zNR8`Xz7c13r5E?&5J5t@i`Z#^J#b_DGQ>eUXMPNz7FMN`P|+Z*7f zqqJzI*RS5;W?W5(jb9?g2pZIQYK)}#G&eISTa@Db>60x57_ z%i!?WuZaq0h1|hdORTAdg?$J@Fsj)3_|$U;ODqQ2Xlau_?ZahxjVaDD51?}7CzKg{ zK3@{snmfDn@q2ogfTiZtw)YO*DOLj-{Fm71?TBwdeemEfe&u%kvSl%o9^0Fi3(Dfs zzK5=OVIxV+gE!|^UVj=86ZtlHlK>q%4G>RsqJ3BRIv-qBEB1_xV7J2;thf?S5i4uK zlRf~jit4sF=|+ld)FT-(GBRv3fiCEyN3JZ)&!e?i`uC#%oiyzg;4U$WGSFgQuGNIw zN;XJtAh|+a;}R5fh4x?Q3Jz;t9=~`|5kX=|R_r#5bgl6^W^z0CWqRj;p4(ag{H@3= zd!RyxN~Ao_GU3*djgu_Fq0E6bQ6zeUxAOZSHa^*_mJO%R)vH+*&DOkM9Ix01!@9uZ zV0)Q2QOJ=g_Nad;$~A2Z&9MrRR2^M)DW4i*M^8w6mDAZG_7JXF51K zl?zq86?$5oHYQzE@nJ8vFOEb1E?w?d5fPDU5GlZoAbL23?!ng9UY(s)ts_OJpfov9 zm!XW4ihCWck5|Ck$LO9B05PaO1X#Tn%!&QF(H%?#vQb%8=W&;I-6%p4N&$~Pc576m zWN2m=o3~x`xs%zi+5a*?Mpo8{>+$`%9GrwC^qwUi0ig%Z3Sk}vvCdtNsQ3!*gh@S*`9ocMuXAO(V#N>7hyeoP4pS05_in>c}Wp{{Lk+2 zKRU-Bud5{xmLfsLEGn#oRsA;!mt`(0UhOe_?9sJ0H8zy@bG(At${P zw5Jb6bo61hvWG!qgK?!NXo(Du-sOQ44;Uw2!0vADp!0H{86;|u@OZR#W}Cy&?ge-O z01!uj-~qD%WReRa2B>peG9c2BuW#2G6cSM+}zyWKd1ZwW&*P` zFVckE0-g1s*{8y)r{^Vv9w6rpT?Aqf@G~~t;@lmrmRqCz#$dQFeu%W#}hB+deS4TvtZ$y1^ZSk$wZ4E>Pf`%zy_3eHKHYD8NtT zqs@drfru6*L{gzI)R0T8vKpZ1* zd2p6tH;w8#&J;*9A0GrWv9iuJUMD99t6&ven)Ts=CV(=@Ct$L`_-ZLCenFw-Zs3w2 zV4{mGh^()f8E9$2gzK)WyT!^1Hkv*$F|lP?QQ#vBVzkGs7)A6{Sv8tpwnZ6hj7d(F zgk}6^00T$x2oaCl(*T_o_W3|f!SjXom(AC@#_lhEV?9-dl6~Fn_e7;ABm;yab zMWUeC0ym@o01-tD`!^I4tI_t^a$kDyJ5GaIKpU{MX1|5OINpEwP$8CY4I-UW_pvuL zZ~)j@au5oW;(=<4mTvs~#DGPBECz}82q+X_6~NPmB8*>0Wb>d{+XDau)FQgm#tk|c z^wfiVq>|YW?b_cuJN5DnrGbEeHa!5-4H)wn!UCoO8Y(>3%N9O-{Mc@D3MK(QgZ%8y z0sKn_GIVO{0w^g?8xydJ=ms(I0a~jQb?YlVfu%@8L&HN7tj*>{6~Yl=qlB)pw!ZkN!vrm3tRoy`F9IKn#DH=mv`o{WNOFZU)`G z2L>2P6il2k0JVOyb3?>+9Gck!hMx)QIPSjB6JzkoSdjT~m2dD2X%26PYHThmQx{ak zSN(p=o%gFaJ!`ggpW8_v%GDWt&FQyjxRnrE&n-1id3(piq~l`C5%UNQCd}lQ9JI}w zGd&4gm0n)KXUaua2xVn1m>P6nEge84B7SQ!+}(}IQ|RGrY~6v>as)OoUN`IqDC1`l ziqC=mU+4g&{Qx142C&ofj*Lx+x_vQ5;-RWRAYD>awqIadmaEy zWX`KYobZG#ltFH1cR*+VvW_hBwd~t9ZayyUc`1^8f2BPW_T0oc>~#u+n`*lpF6<}- zpXLVnEYT3Edw7T($~0&|j(6WCYN_VsPy)4 zn;zbF%VaI~*mINPsLvDjyHB7!ceSd1{Z9U;=g3p4Xz!#fv!u4k$EM{k(7XSVtXtAt3CpR4PXw&`4*vn(H>qL7&SBS78bb>5A-x)#d<-7Zyd&{f4T)S9$4gN97F&R zcuc#fc1BO3!v-ySg8GpUF%B*YBMPnSMhDd;2syrjeNyO{yrduaZF15Zo*x!BYydl0 zE185HR=2`4vot<`cz<613fh6l^QX~ajGaLp$D}tGsdak2rKb*Iy+`(9MDSvf{bg;= zqu9_+4@+%G2iLMSi%4KsBs8o@j}{gd!ZJo41PZ`J7xZspkYJTbFcC@})1p{cl}UL* zKlPFrWys;hDJnnL@o!qkamv{wK}<6uu_6vHwY1Q;U2Kjie-+2HgvK~uCZLlnAfR<6 zCOR5YuCydHd8{&`(>U}qBc!F9A#jFmXTf3^QbEIiS5Fg@1xO*pxfGd=bOKP*Idai$ zhwvvs7(4~trJ>Ka+}}h*7)~g%Mb-RT&YYhEd58qsJfMr{)~#FDu3jB-wi>fShCMa_ zSPcF+xCcNWc|z;3N%`6;rK^5#=^lD^H@93ZX4BR?`=_ef&3nbo?!A($10B@2>p260 ztIDrZUds}Jo)Q@PGR30MHXHV^u0hB>~zi^U+_{3h1@%= zw}*&od=eO`XI9W$ho=NO%#e+NfdMF;xSW{w2irzFzcj5Lw-muf)PhT3I7-q+wL%v~ zYt>5)7QTcZn!&QRFq9^`iE_f=FS6Z;@w?u>kIy{-`SMjyfaB_r9W-QE1&Cy6$JeAj#UBQ-{rY`4pEh%+mm>(~t7jlT{0dc@(;~<5khz2_wmU zR2Am6BGYm+yntq(ZwStbfEEB#VuKmIwpYB}dTdNeNNxN6EtQI){tX9tuBIhou94I#& z9eD&lFd%bgb8g%HUDKc7l7Fnpm73ohEC;oh(=GPLD%BeZe8Z19)FF3JxjuaRP@*3` zcmTk(h9oKhf&}VjM)!3Lrf%@2prfd_)DTJ{<)YAI z89N7O5~3mDJJB&Y9ehKkUY=n&w4Z)DJ=(IGIdi_{=xnfZ`!6XG7j?OMv_WyZ6KGF_ z3>_})I(fm$soRKHpH=bGhpSVPk*!WE7mxeu<}%Ril%)}*H`1+2(r4Nt)YflQ#1j%@ zRg7`scSA#_W{I_&B)#3x#Y^Y~emfDMV@40fdM<%CA}pED)T*Z-g57$p>qYF=i1xfz zc$&Wy?Fg(MXjz7-dVKIOzmXQN8S2c0$4@6V(U(gMPmN2(PhnS6wlq9c4G&&BFgal3m?CK${p*hu$!%19~z*3Bb>r%8m_u@ zr6t1UzWEC-Uft-oA6g3gF+Ef6I2Q1n7@LV8Xr_X+z8z7d#GnrOQ7+e9Zf0Wv^L)mj z^7QF6SZD#?#>YKD#;~O3=j0O-8mXl+1NhV#FN}Za0;z1#2Rn*^YK_e%BmaHGoxpDtEKO}(egQ?^2lyAcZ|lqi|uPY^`j|)p%=g4 zU-;%REK;!AHf*d+`Cd+37pD1{UuIgGDQuF4HZ4gWkXXXfRe z?*X!eZ)as?!8Qh5CFoFxoibVi#%=Fl$#P-Z1KtG%ICfuMLnBQ+6CNB4+JoVX=V7@# zV~|ZKe$%=|g1}46WG$2~5t-%8p#j7ca3;5#Ji6fmx#!;`BDSM69nYpks9w) zATOES1A~K6p11S`aTSPYEl6lBWctEcySaZEsu{n?x2&~%LBlJJ%HcE3;H0o8dElk836jnZ zpo(e`vq9V$$kDOr*#n~n-7^Obwc{1e!~DDe2Y>AD$|)%5Rl3+g|9XPkGULoIfF1;g zNDHt>`>S6bqh%5yp>ptuwF7l?6GdE+paQ!`GDFjJA+*rz>Q2C=$H1YrzQsNO83p7~ zUDydd9mK9)eV}pd50NHu3z5Yf%q(}il}`~VelOf2Gr^NKVeIvw6ZUiYyO*?fF)^^n zLz{qd$5&JwLK~okT3uIH7nA_>rY~EJvk6JQZMnW}HrE|ZWHDb;tHtkfbXv;5+EI%B z#FNIn(R5enL1&`}V<;&`O?ths;yUGRioQh^P0lz;~enElR>3s?o25 zi%IscmlfJ$UbQoAZqyyW|4A$H9L75VXt+Y}Yp9}5e_2ql13&V6pLkTTg>g4t56|h) zH)+l9HO5DorMRWUvo$ePCl+3**dMHu`*LJ>Ha(v0(vvH|+D}hBQ=-+SNyfN2%(JFEX@5OEyK-W$^EJi+y0d!@dPkm}&0K*|AS#$C$bL*vc zm{fA_(#cNZDVPw%QT8!>R&KDWfdWNGobK10g4L*0_FSe+yV|6af-k8p2EI-KToT}b zL&HT_7v}2*KG)Yvru6WQewP@RdGv_(BW-V25^w246@#t`CECwG@aVPzTX@b58#CCM zyuVlB&#ilp%%`#ei-l0Z$NlzuJQp6)G@v=)j`gN0P{KAAxJ#f=g@q9TWMmZWNnn3% zz;05&0&(#cyS?NTTSv5V@V{SWe))8moR4EfW<(0Eyy|;eWrb%#`t8k!mPblP2_2#H z7r)=y`VR27B^JmX*nP3uIZodxjq4ntix});azUiz$8-)o|3~zsoKNe4y8(;RjWzHg zsTFT~a1O_)4xF3mjg3h#8GxUQLK#A>O5u``dB83t_9> zZ~$yi%rotVW;O4fUub6^_#J%A33@NsNLIdo->kjHj^4o5cz{L1*5;pW0A4VdW%N z_2!Km9GKy8agQX&4r$&iLTj~tJ_i1BY;A4rpsu?W_|G>77PegATTEc9iw!iF>s-26 z-1FV5{qFruMLsv^Y=UAWC+Fc zwA5j_ui0xR{T>?h_dUc^%3ul)E}vituF;7x&5ZE9n~;=JNgQiJRZpFkQW^cmKTs9} zL*16JtTafwTu5A|UOkQ381jv)F@8k)aEynR4jV%2O(tuphJz2Y9lp?|7Qh8Y+$N&; zbBfEg?}LRQ3{oA?%9c!2WF+Wy=&pI-fdKm0o%^Z#2;${I%IbVf@Ny0$Iy)$2pq<$m z#6&=~RBVNYxn~UH&D!-R z1>MyNlq;XZuPJMb0wkLS_!FD*=0osMts@ zOG-)taDz!9GNgVEER@em2)I^`!iNTytbbJ@(nhNp0d%0}-);E4Q|?bXy>ZPTT?X_LKSnfQIdt zQBjE97#}P%o*1ZJ#?UMk0b4AfE!*$-wk8YPVKS6P==WnpeX_6vnn6xTh!C!*0GaV~ zjWH>uNCZYO&V1e3FZfKiZW#c|0`Do3KQJ_u5^5xPzOc7NuPW%~%8e44nX==eo_@jp zm8Q6;q2sC7gOSwO&v$KA2myh``A7^*d^U1#iqsBlNs?Fn^x4&T*sK7^#5se!<~f<& zmZ7)qpXv5X@rdF^y(}1mHKzTFA&keNzf&r&( zqiTNw5HswTY^xK6mbi_|oen^5@FjxM(pN5D&O5aIN&;|N5555f+WnMORCe7&h3h14 zv$z*@&TK)i!{Lu5ZYXl#!VE4+oxw7S* zRIL;96U2kV=8Q*La-K+h!2;KdcgbGdCG!zkO(edKfnh1z5>@DU*>dIjHx%${TD2Iv zgkeLqPYIt*yC0w;F7~&BWtp<29r%*nntJD2gs2SfsOmDSiwIYRd4C3Myxp15*474- zY{)iLLNU@rgn{AVN6zsF+k?>jI~%TWy)4rnG&Rege0>3l0#RMgEGT%;f?`H%fUpl444;S4V690?je$XQ5Bj_kBqu%h z$DxllbNmRjYM15oRM>3Zu3&}DD>BP8H@Pskr?ns{DqO`8HOd9hB z1C~Df?AfzG4fkO)cGz&tW8b@5AhURFH#0O+Q~P|iJvnlfTwi|koO7|+_SiT}+VyAG zncvj23-Gtjr{|n$GQ?N(SmuIKJOUq4g>5!~Hp3T<2XfIkv@Y~bZ@#|^UsnQCTL#~h z(62^00$c-B$YPsGbffGc76`CRF97q3*ul~S|4bmaqlA=hbiXf#(OSd$O?hxtj zl#uQYY3^M6{NvvLIv8i1vE3V3-}lCxPt9M1hiXoFuMg4KBfPr_56x!R72H{QN5dO` zkvvBeLlxNQFk}^AMto@t>GD(7$KL!}X#GxQTbHeBX}bD30peoTL?T4dbNK;_f_G2) z2-%+d!=0>*n?=Daq5D)S9~b8un;zDSQyXG8V`Ec(%3(MBPzEd*=s1bV$P64u+CbEX zNhWv!I}2+2{Aw-~lyN*@OLm*9+>We_j8bIgpoG{3yB2690!fpm!8!-e{T$*UAVKQ! zn8EcqVg}(@neE@88k@!bqo6U5^nv1dX9~|_N81$~Jlcb{IFLrOk}uxVZoklOCuL&b zV)esw)cB;FI3aEp+nHQ&TE;PfE_toROA`9!kRfTyTleezFhChE5+cHWin7GCcI?tp zTM!C5mdZRiJ~>rgK}QG#V@I`ccVWMX^2O@;rf~CHtAG+u=#age3n7yyArI$P%Cb;F z>lboKyW=(cRCQ9M?C9w}~waiiQ+(-IL_`0n}&*16tFe6OUaFCu0EDG-owA~#omrK3$MY_t;xV`O_v#+SBUE_~Tp)4jQZeu3*5Mu0d-CVR>0uSA4 zLfKcb_jCp-Uy(U8S+|p1t{$>@2WSdaD3yOEr&dikM>c)66r3V4+NqkuTQ6?pWxPHF zeL#S|2&Y_2`P2VXAiUJpj+K#t^fZ5IlYV{XYFA#NGx6RQ8_yeN zLK;$=OxyuY>vD{4cmC2?{zk%?K9jt+dC?a+i3I?)IC5z|2z3Fd&b`KI868T~&Ce76 zfjw04yJs{|>v6*0Bpa#lpq`?-R6FxWv;33EBe{h75vQR$mUIKkYYplZi zL}P9Mul!BE=k{kWdjgm;u8Uiw17C{K-*jaP%2`EX!jcU$dw#awHCcb_V#sil)Ij*= zjoHKeq~``RaC1C`T$(ZKBlyP@QCXP8ks|qn))8oBlgVuE1H<154*qt~xzU-XA`KWzM*4yW#gFRQ**WEqYuq$B9ux=wJOc~xZWyT>>QqUkI(`s`B4SLJ}@ z(<95pezG<9gt$|+nkp!!n%Ta8t%hsBhQg_2ZX8yD)z>fWEns zpVU>4iSfC#I#%{j&93i_qhx%b!Pz}|L>gC0@$p03N*pQ1Z8eRL*M)G3Dd;r!v?c6={*LkJE{rq3WSGy_`$)SeoJj0N$+;mH<&HP6Lsxb~-mAgwX zanH&~yV0!F(aK~IUM_|j#{INMn;(h!Qq*PIR)dZLjk}6{n`vUV`^zRBXD8*_{QVU@ z?kQ5v1;nF2@oXj)e5fcSgKH3wi7>%+#{RdNCxuae9}O-8awI=oh9p#6K7pRkL0LD9 zeww-tb8q$3CigXPDW`I^HiK}33JVa>9te%vDtJuH7|?O=Z=0>!6Wt)XLmQMa_&B5@ z3?scxmsg!d%krUBsaRq0X7U@ap~)GK9^%1r>Z z2cAmxuem{|6l?~I@>nCp=g-_E5$qOLgx%^y99BMlYX3LccC59=!nzTE5I1#bqliT1N-w{QQrmSKuk$wr2_a2Bkttc-zIwQIwg zBNmQ2u$qIs9?;R#6RjeC4sLE|Aa$WOfUXv*MB}3IzCMS9G@B6Qx?6Ba4 z(;}1Q1ro?S;XKjq(DRARojYl^-s-1lW}}7nD8WQY?$yv7R+DOn!#zS+1J9N>HW>D(<0Ewt&RM3~-Gr94U zbZ<(6S#obe>Z3N1fr@4tY8FyjI0h0fpSb~+FHkW$LR%G83Q1Fva7E|78vsB7ie`6w zjVmA&1wpa7UBZoYKrF!<0#yr{#tbVs^Z(d20I43nXML*|ihPI&nsr(D3WP$SIOjR% zvU~tTsH2R$pl3L5oT%L!U;)wTiESR2ic+k2^?0pg(R%N=jfo>%{VmKe-ZBtxle(y#IShYnY6g^iotrF_*d zobX(x_7VK%sHx{XOY{zIHv5}Rh;KKCG$&CM@yfI>2jy6Z?ig*v!ME=P1LAb8HgojB z=>?d0fphad9FXEofJg|5N1PYiH9Iq|D+!%;Oq`sKu)v|kI)_&a-L2*M0Jn!ffK%Ya zZ#QiZo+12^*Y2aOku}=J5Bh)HZ!IIjnsOl-!MhJ@o{N6$OxAc@S{81f zBCM&K%(qT8!oQjq7B?EQ zc95!OK);22ANQMXqoE(fc=aK z364U#5%l0z#(;%Dz6tp6HF`!K1|T`1!nevyY9SwJ-r}{BpyIa}Ttu28OIN=92KoXyGElHt&fM&fVC?nF?>MDX1; z4UsT!F>3>tOI*l}M)Y|Ohaaeu;@ZUy7rNavZ;ZSUwkz`w%sP+r0Y6p(#`;Z3g8PgshL1{fC46p!xC1LmDM_UnLt2fYrhVL=2taN`H)dT zPtV@F4IIh{)Z-ftycA#{fo9i)Dxd`{%KDH-3uRi#EGU`|fE^pJ>LLV8Lu(PyaODP; zLaUiRs5-&-uXW&+C=ARdY}%pI0cBJ+iRy>eaqd9^zo&o0S2fQUAls5cEWjtcJi>Tk z(b>{s1`lfV8!W?Qa(EVFU~Ck80m-p!Y;16>%!2v>+WV?5 zZ2&DmcX$o@CoEEaLc5fWH~`}zXb%K3dq_fPzmq|?-W;om736SdK-0)~e!=!X;XhG0 z)-vfgx3aJ5DA^q2aC9o?nw@zZ3To8bW_z#4p47=(aH62dR{vt3#**nt^ZD z7<>7D%|Bp)D|3p8af&mqOzi!FDl(`5;AUondfhr@WZcHg%*=>2APN0{0T4r4q(SCX znll1@F<8zvIViKp|4TuGmo7PSB%I6(f-hT@)DbJ_)qCJd0*h*FJeZIf|A|F_g#29_ zsBvS4Gh>Di$@0Dp#I?O18kqp>-r68WN6w^QP$@%*Rp*j=@m^ia_Oi}r*Pv-sevq@= zRtkU!3vZBRZz2ObfZ;H6b^EwJ$2AOQ!_}e85Gn=WqQ`IDxpU{BM3-Fit3SY-UA7HG zq@?<;I8iMNdmv*S1Zd&-59VX8r&!ka;YiDJz8^g2)}hCBAY41~dOb8^ocDrB4dB}aXY|aZGAt<$U2q2!BjxjONg@vm3QrU+ z$mpv8h5RN2Fr@2dr~u@nH7Ev7TBNu%4?fogl|D3N;r0S1?W2nsx$g%#Lwqfs5A$eC zzMj)l3;8yXkyD~F@``%tCTT;!__XHy+Up39QO&_-oq6SLuG*vUmzl^YC1mLUSq_jY zd4Nt4O3TXn7Q~Z);J%rut#U=p!oA-FIlT;4WwHL%us-2}oIDQ%y5}$TBTo=W;g_#N zbb~xK&_h^_u#1}y2EI`2W2X?F}KsYwR80G!}_&PjPJp=43Icrs*Rv*%No-; zboGLVzykgZK$Kqb0cP>R&l=8HFwARKWr9u|9;F(+y1jkzX}#k8rtVlyM!n-na)DOvfay1ss8O|MSqgx5CjiO|J^%(p6hu6ekkr{s6vK*T)-G-5 z$3-2khrf*%^o;d~2nBfJw{M=m>@g?wi@SdgspN5r-x2;ww3`5U0#%tJ_C<|8QjLD; zCDDC9ASoXxL;gBy<& z*#nS|^Y}U+8dHly4&ol*7rK!;qz2GqB0WXqAA*B}p$zea}7d1Ti zw$TliH8Md45?WPNRULT~q$wc+9;vuTM$+b&rjQ~lPtYkQf*}H&BDuuqw?9DyRpU!s z43}nW3Mcs5SR9_Lj3=iy(1|$KviFGx(Y+tFR0<1?KoKU9f5+gWndQE5;LeZxBYKNA ziL<{nL1s4Gh;aqqW)50fx_>>=Rq%U58LJroq6q8|P#)BL!=#T64Q;d)Uiaj<$tbcQ@Cp2<7(t-HTnhv;r!m|pcV3_3+&a@)ZV;dcIO9vIF}g@ zbR3`_P!ESo__u=PFRvLd_wCNeu5~v&C0`J2$LtI(&Cjd2G8~jqj%Nm$JzbXU@Z>u9 zCD3xqI!^9ne!F*TqWhM(`waN=fqd|U`*U=zo?Sxwp zp72);U~@Gz@bM`}#h^m?!EHxc6DeCI8BDkbMOYc=f4QVkHDIOnf`%17-D{&uU>sma zmw8`bIgv!FgEoBm6b?onfMxVBJJeSXT|CM+eIkGlKZH!^O>K?9 za50F0VIYC8KawH9#cfiUqGMumbZ{^Yf&DcG!0P0pJ$TMRn1xiTaUcW1E+`{m_d{jF z?{)Fwf#dd+E13T%LRaCY-Hb8-;Suk<`Ok52k9Ay;azpbT;}94)VD=={*Q&`w>zo*z zgOQF zxi>}vdc4x758Z=t$Tb0)Fj+>3B44fRfq8(qsCYALM4Xrvr%VQAq_5NZAsFt(#l;C% zXqB?n#t;S-Z!sQZk!a^5v+lqk$lXV6WCKqDRI*pFB?%a{eyx~%IXlY&xh()Y@NLgQ zrjeGKI(`_jeB$Ayr8SNe4fn*V&uTbVp(p5qi0+B}O3$MWS$NiK#6xtYzSZmL#)YyU z3o3;*pJ)=>sccg^u=etIc{)De9bL4CbOlHQw{~|v--Wr z^DH7`SEHk%nwWWZBTFnMRu>jwM(Sf?pV7(X{G}dPgdmR_HAjlfKg2>-?PnRVY06QG zJ1lcZB&0vjq&qK~w)gXIPUD|;GFu`h(7)cH@pSuVYEl;I8A(JE^nI^iAD}vzB%ss5 zp=F|-e++aNKlC@S@YKT{QVoSX1iit%1VZp(t1|ecU@1L2T4yL%$+dZ3w-ya9M(K15zkv7A`3ASK61E)1>jIKHsRoEhP?$; zz(M42Yf#)eF32^tfSDZD-CRKmfJuD!fdfLxf!wqSn|Du&lwPcF%4i>PX46ZrWsMw0 z%Q*??I|)$x1?C5{-@UEgW~-1^v84W8`fiY3umwjf=;W5+BG)K`V}A^I#|BXLcL(P8 zK@c4RVW$`JdwcQbtN-y1)Jya}u|a*r-aRAu?54A(!5MB#o)Y_>zH5v0P-VxD{x!Oq zpe8jYSm>ZaI(%frU@`}?XNE1wsN-t52$iy26eAqZHSdf-9t1xyhak)+_5>=@hq$-_ z?SKB9gVQm`m*EY>Y{S32`Hqvi$v$0W4Sqp@!nIvFnXxujSN(+>@&S-lm3eklMtz|#3Oa?&h7@vw2VPS z>ql+P`*Xb++XqBT z-^s;*2Zc)odXomQydGSd!#TGLH$YX*ANwFR6nESMi5n`jDA{ZBMkHxnYAZx5NeZg3 zGjrb;bGW*-)|1nG6#>3)PdHLUJmZ}!^=rLY_xRnAnuM^Xo{Xc1H5jfTo>&}UBM5)7 z31*)yUUSw))^2M=-OmxLfK{+FB!X6rN%s)wO$BM%AkJY^_Pixz)b*_ll~<70Zp(+p z36F1>K4EU)_Mm;eHlf!Wh)OYlVX+9X{}zs@MHuAQXPIO_!8{akgtKs z9i9sp0+^VQSub$Cfoa}K_c@H{fVxW8O7sJoLObr6T-`;6KJ}7Y>-UTfcaHbvHq5q8 zKY2B~R&50Ddls$X;kW9P@|_nQ_S__$Rb5=Ba{3ol=^ij$*G%dc^%LWcs)~w@AXip1 zbgaZkIlW4@1GHsjZzUkq%=0^!+u!5ZX^dC5Cy2EYl+y=Ma>?a>_3a3ebKkNq9H+lVO)U;c#Z^GW_LbyRMSyQxZIx zT&L!+0BaCBI>xVSD;4VYk9d#XZ9Uw^AN^An?vt!s%K*>QVs(B;d6*8KC$~ul?K=J6 zJvY9N5CXx;gn05Zs^`u|LsE9#{%y~=ohc|H!_??~W(xUcIaDSFZdCaNkFzg*YGxF) ze)7vRPrPx#c4{|HhRg>hQxv;CH=tyvgRtLeKr=x&0xBgd-763=GHRFJePx{T0C)Ff zQ(P_F{uUcgtjEDc2~q5n72}=KKq$(s?4NU1jgeikGt2g{X1~wmuzWR-26B#y@g}*= zZK`*66!y3W3fGoC3X99jkf5I=fDHNa`Si(ZF4cN8Z%##PxYtz6jUUS$I*4Dteoa=r zS@3&dWBZuH;=4@8Hxlp&!Fn7k)vk7nNqq?qQI=c5 zpq*%S-r@-)ma(X^)3{WY?>l6Y)@ARYtWSpka8Po6xwv5cx**nJaL-4vy)90So=Zb# zPBUYm=%sa6oNWZ5pIQz&xBPQlgpwthA`$uWo)=xuGWPl&?b0WoTV=Ob4)3dW2s5HW9Z8V9|H_T+JYn(SA`_`?7l3*|WuKd{!S%+!iR*sbw&v@5-eRw6XJ6cPX~rS4I=|j-+LH;-8V}Xvm~{Xyxd?FO|72zg~LQDcjFZcHaWsQjqGOw-tp~Z;TW& zX4JXi@<4HCU`#Fv#d8c~j?WkjqtC^D@2G$HwCW!+&vy0Imq? z6c49=fj2#>ni)P3+(p0Y@JPz0=T_yTh}?+ufxoD18;OG)l*28`_>7S_L>ap6)Uv^u zKIiSzJ`|FN#9Z{OtgM29GnMM#Py*iQ3F#iccmY(TSl=3{Gr?M~t9@$$k>O^>1hQCa zMV*LDCNQBGdX0jm@aGE`!X(F)5ybZ40 zvh_eviK!w}!$Ii>ew0@u<<#&<;Wp(q$7<0Xrk^B>#xfM1 zV(COt* z^?-lPdCJQ!#H(jpPV`DW-WLn|Iktn-!ynr}kd8OUr*tWd2fvQ2O$&`NVo6`_&u#af zACSs%evgmgG~E4*EDdAWsc2~-U=l#?HK@)Z$(eI!4k{snR}h;_k@EcsN%nH~>U}ho z+?+%&>kdtfW7^!l?QHpl!s#dUUI|gra^AM74P3r!WqTn>GSbqZ;H|hkK_MpL4lRjm z=V@+q7;p5I1G<+tH z7~43)zm(x5yhLTwIvo>FQ9>H0H}{_R@#7K`Td5xqgBy|Yu~!}7-JryTL0y(cO-Qv4 z3?m??APtF*j}OM)eF5ij%q?e&6Eup{QR0Dw!)j6mv8HEXft?iT%LYrS_BXSaydQOfz4tItTm9<_c=u!g>ON*NyBjZb#p zdf1HZRR-sP&NjnX&Nmfi+U}WCK8#$i#^rSO9+R#fyPF@@-{JFukZ$rt=~D`9yT-o5 z$h`?JET-l1AE5fZBpD?WdZiQ0@2LLCyTgC zkb?+mYn1T#G_7NdcyOuZ|Nd;OhX6Vd5Jq{Fy8(Iwmu^uTG>>99qlEK?j^-n$)ubYN zIJ=~+GGvl+WEhEpUN&b(d!3GnFUp{LD=hqe9@P^=0^R`kC(5^9Jhfh z`b^vT){&O6F$L1tllrag2B+VbVz$P#35&ler8V;uyaW=;nX4Xnj|g<8r~Z67JIt)V zq|4(AXb~TdvVhV6iK!N6-vfm?Ach=?0tw9zAIh~?wf@P`h_X+aU(I*bMq^NAE;Uuf zAq-$vR-0J23a}h5Ysn}XuHbfDH9gHpiJBX;xn*3RqZg{qKd@qX{DO@wG`^kY7q?t^ zbVhI2FcKRn=BT<;;xz4A8`)mR6?TvIFh|@>JC(iXBH}X(S72W?vdEw$-7Jc0kCAe` z+@Agk^WRd2)ay`Pp~Z!JcVYqTOWz)Ie-nT-{E`c8@La?C;B@B)U;?lOP5s$rNLL2v zZ=8hB2?uFbLb}proIU|@_4c_hpyB3Y54iqY{j2s~&LalPLH~LhV!NL+Rm7G)5Lf{2 zcxipz%9g=^CU&f0vr=4)Z7pP!YsQU7;${biSqM8WR=U_UN8J^vi=-%JwK5 zoBoOLN)qTf1Swp^=JXaHi+;Q5YB$ziTtt>lltC7}=3+QjO)znH28*g@Ls-B&n?@W~32~D;i7VY%izggM%4|lbf zdI5_$ie8RNuA1Ox75dH$FAQ3VQI zz&C@(66H?A!2a=JATK(hZ8Wwt76-QP2l7|MTpDq4@Op;uiSwpr(x`{g6pHDey^1vQ zl5NK2dQcM?7c>fD3Y>p$A-%o_Rkr{sAvXa$t<_%(YGXxYn=X35&zc{^tdvhg|R1a9K|0gYMA{BzHlLX$89?{$0 z-q*XJNdSu%1VD5KEo3xHy{M_L5*MlRM|vV)>!>uVmMM zEU%Mn&6DIiHsJO>#}SM2X760t zKX@F1s$wRmz(D3+#q4+2F|aRfG;w$`oZ69*L59hVL+4nQmug89UtwwQ@Y{BUo@c6F zT^beE5+inYd_2p>)+F5&QyxNxU{M!#Qx?IX3cM55S z$&6_INuYV4^nCL0p{O{_K&?sI?PzWIrdww;7_Z2Iuta?yA1TXQ2p(K=JF-ARnI!|Y(t+$~9HQF>c52Pjly63Xb_&CYTPWCs!*yK~|EGUA3 z5MKGF&vo6JMw9y=8+wmQ7xv3pF@!x)sbRux9*8& zFzd4w%nZQ)-V7bVwHOJc_Kh(@jWKGe+S=y5i#Q*c`105rwOak0=`kE}YSE3t+y&88 zUJ9(9EZUp_pMSe4HCzp63UTfgk0iqcGo%3mFe#fm!-Zh)umCLp;QR=gQ-C3%#J#Sp zsscHfnYlUf3J1q=jzRdh{hZ^Qo!!X_8x}*n=xfqVQUl*JZ3|0?gwtTmPy=Wl-V9Wf z!RJ4jao?G+V&$KA{VxV)>&Ykimu%v`)$@#w`ah}v8TlU%IA~|2nDZRvHKGL_l==cR z(%n~mm!xaCPqtG0j@gjq(x=Jso^E@jq#lTPaH)>LN(Vw|d~p+u!;qwyMJn&Ngbc+6 z-xTn^$Rrn_BL9mY1auI2l(Ezusw_-o42`-hzn^UM>n1Rbx%h>Piwh!!<1Q8D1m5;` zp|k&vXz8KPS)$Kit{-ke*eoEN6r{&pyb<$yW@#V)zp~3;RD_^cWmtyyu?*@@>!#f0 zIhneqY&-AunecRWzF@?C(jGJ#PEJn9KQ=weqJnfHW-@_wx4rvYvtrKkjJDJdRcEg_ zZ19fO<(6zkZXJK{{((6FZ3xnisq-fXm^X+bu^G+#&Hb+IrD#M38Q#f&`>wf*ic{GG?FT{d!nmoB=#s)`?ud6e-@kkU?kqGE5pY~YvJ#E-uNHXOT3 z8r07ckT`=(wS7z}K9iKE{9EgFz7J=|Fmb~AH4z4Bj~~+10TNqArZVHX2(Wp=O{P=f zb&^PRMY=Vs@a3QCx0XF!Zf7?uLQFD+dZ8dqXx(}jNqW-pY& z@}$W;OB63qLwIm_Yv|!;nVFPH@DADBFWWRm2yHk)RyTxk*V~kd+#vmVh8We~j#z!P zf?kjSCAaF^vi`FM9TlHD2wKJ^5iJ#a|3<@-{_GEmwgO$o#VK9@CQog?IBnhD{r`Q$ zSYyJ#ASSsBx1DbzFkBO^e$P8fvv2p0%bHH;eWXen2i6>rWG5Wk`L-{Boj}IP7CCdf z3Fz@l-8t@GTSzZ=k+D}|g^%~(S$kUimd`KhUF?!MdOAALnruPzJ|I!r%Jy2|pg?8^ zg8M8bC8dm?KTefs7^HY@g9G>$K}227(FM%SMk&YSSk?yqO@cg|m-aO3364nSz{|m5 zfGHtTM{D1o0Fr06@$t8bM<-KwbbcuB(^hOoI#ee~ad|}T`N!4>#4-Vpbk)nXQH%e# z#rpqR>x^kCgDnSmj$ys=KWlY!<7%o@cf5uxgpO7f*sDldUq9fF#q0%IyUX^%I3IX% zdl0QG#lt0U^2PG{O|{p^AVI{<8;&~?O_Hi7!eP=m+@x7|uZ*~ELbOo0-HGAR)sv9H#j~rE{DwWsrgXxCpLjI}j*9zB>$<0J{^_ zgAP-(65$r(cTinPetR+jxeiuBm;is2FV6fAo!#ri0|N9kjFg!Kv^8f{zHWiK3<%xy z)LbGnwh0DHyl4e&hM{ha-mW=id9L9Q?P}Zx(w#G30t6(#cDCLQF;Ck|7mApO!E=m- znsO;9y~s;5JlPO=3m#{F|E7lR^vh+CtjBK|EXBz2zE1B`lw6?k0{5Sq)-z6DtnHLr z?zBuJgD*))ER}9!l1Y%%7%FP%H2%+)S>+HcC>3t-hIjIb?Z?ETc~8dw>jP&T9v*^> z5)R>O^MA`Zy5KMZFY!6#w!E5Y5Jm86x@^H~R%;ybunjG10va3J83WT9b7Ol@|2Kio zpZJW?funAz5;KIEQudL|I{h4F3Y=_9bDt-0SRP{7On76?JyQIC-!^{$HdRyqR%LNB z!r4xz?xqB7YJY**oaXFV*=0-ki3OL|{U(QtQFd0pC$=J0YZjn>*Di6)@NW{`5`#U7^TpN! zv>E&mZ5i%tT^=q6+<&uyXhFppU+oT3U>1U{cG`MPyGu-e8V7n zh^~gg744fWKZT!KfJYn13{azJN2h(>2kCOmbD09 zeiy(RPRp?H@C7iI4p5EI^E$?3ZKedRw0~N|5Ethsb;L?DO0(!!vVU8cb?=AkC$AnZ zCl^{vd$pEqOt`QePInH^#E?kx#K|~#>&br)pE;IM7AmFihIyw35 zWs;zPX*&*bf>Jp!Zm%G1LWW$d`)k;_&HNbnt45FM=7MVYDqcUwb_tlyWy`U0S5m?c zH*26Km@IxT7^3a2lEvcaN*HrgYB^s67G}ux9;;0PiL9i!_;4**Az7`9?SC%8uh%Kv zJt@yKp)}OvRiuv`>503eGJ7@bjN-T{e&JTCEuJ}qjK+gOh4U91iIh)iOh*e`RkJ z9Lm=e+r5`vlRZmo-<*EJKK%C?=`FE};x!E}+jq$h^iT7d z#oT!_T`G9+;XAKyl&?V?S4rPpYf4b@0uc?pW>Mi4Xj4Fi3fZ+ypks{)d8&PBSWjpH za&Q}*atctqA)|<7#NWbXwcLfmLc>i=78C&%H?V=;z!9Ftx!QDD&F8=)&*Lt5{-`&}ro!|zO) zLGj@*A_-anej*Oyz@h;cj0sK3nCG_cR3AbLsC+1a@c+BOxHF!g)@ z_mF;Zm<)l;QnfTRgj_@hyg-Pc-_G5u008-UHt3X)GvtqLL_}`XY}mlP4sH1#P_~u7 zgiZpc6T{fM5i;XAr{SL@b*IeGE#9hG>$c3X4-|94zubEaUx2X~rLAQfEv{={!h=8= zrb9jnm@f$t+6H#_V92TM%+50)6U!?9)56p`8<3jM|$5vYeuQif7 zK1DK;knsg-+eruUuU{q8*su~c@TBwib2~fv3E^=-5fCe5BrDqq=ohq5)zAS0>icJ# z`RiR@Ji+h3@2D7&lL!c(|5MVn8OeV$ovy>Z_GM|fh72y4x+WRQ;;vnICYxwQ=qN!D zfddQ_aWqKc06n|^m@D{ll+szTxjmClewD4NZG5n{jopfF`;peeFOCE}{~R$foiR4n z29=q!Z0fXpp5iMJ7_86BvG_fBKtx!b^v|Lks)x>E7lvv3NUPP&TfFD24F%G|;xxhX&hoRx8IvS!xv72+8oaAK;$rh9tvYa4{E zSh3Oqd->!4ux~2BzA2VeSEW5W+7yGD5mCRmY5ZkxVj5B?x-npD@WBaRrTb=|-~bvR z@UU~g!j6Ef50Hz3!fo`o1(Q*|OM@G^8fdc$xt`4S!_c_osDfes@5{9kC9}z01gpPD zd0m6mI_YEa@cWM&EYD|$Dg6Dure;d1;3Bk%7*KvK6zF9zM6ZrwRy%(|NB1YRf0y=G zW4Pd!uVd!7gbg1T|3{~g@K|)SJlIEQ(-;nF^w(8`Jv*+?8Sgn=z^Xhn& zOz(Ev&B`3Lp_n9!MAq&qlDLx*#ldFgG{nD)gpS!5$ug#ME(00OAury1lLh%*gx(+%8WvCOx4lMc(0gP-#`X%3VuU?&8&6;3G zY-{cy+nojIhZ)uWfM;-@=+)^S6*vd%9S%7kc3}^f-GLhX@{Vc-sOwS2yCdZsEXWf0 zB_+TKn6IWHNi2~_c}I%)5D)&Xgr;>1f2AFczhk~%#X$08%3iBvfzrLg*G#{yP;)Hu ziCw)*tui8*hV{?WaiwFGLG{()v3$8oYb}Hi-E)4U67*_)L-n&E&PUP)nG_z z?EGuF^l{j&(&f6tlzn!BdC{Jj%}+8oC76S^FdC|oh&$>WY=o=xJk>7W7O^;1>4>>v zt3N+L3^23g>egLxVvPP0dxfyQ4l2Kt=!jfaWMN7X;9Ko9i+vHHBPkFU>6!by zKYz1rpAc`@MPE)Htwmg*T35;r{V1*e#-W4tezCC*Cv2e)4!nU?Dp9UaIVaYa?eDA) z#6bZ*bJ;%9gSC(=xBQ?2KxkK20qsjqxOHK-Lk1Ml9eD+nb=(0^f|uJ&82_6S+&e)! zPNKV(Afaa2U}1h9czHWrj=yypI z<)vZMvo}PM5&glhoJcinLD91h9&i}moC)m3@R1hreYm+u*>J*hI%{)X-a@W2GUbVa zM?KQ=sh__+2Cl7rz||YQS@a*q;RT6WqJoaQKe1*w1S0(X$z|Kq7}gw)j7+)mbH-L8 zRjt-U(e&@5lmu&9u% zwX8XHdpg&|fdMHcNU;u5Nw?u+^;w)kS)y@Bwmz0XHbX_f<+(62RTJhkEQCR{%r|1r z&dPLv%0MRweDD$sX@wN}uB|UdKalq?5N8mcaRo^N08N;LlZig!b@+*uu(suj=lTiO z+QDmS1R_<&jrj5y!&Xa-gm-9Hnd8N$Cka+2SupL+_Pi_bDWe=B^GGN(%|yYx<#=Ok z2k;-L4KT2=5l%Gak@4}Mnq7vrq1lKDa;5kOKyIqrev~*bQcEFWsq`o3{p*yp29w;| zywKq`<9IkY678x2p8a2%kS5d7c?0rikW z?ev6Kit%UT19%ks_E7D?BHNzMPQBOEf0XNRN$+{i>5H9il$IrB_wcTjNhwM{t=`w4 zTi&k-KAWu_l`dghjrj5UYV34EH4O!nRauWHz7h@27hf9x)?@|9!%!Zp>Adp~uY)Ic zUuXppu&iR>G?d@_GXFaG{5_3r=_ysR{meIxJWfpa7d~au3YuTdyJot7uA4=V{HW7u zh?5hGq2W3pL=kRE85=svH488xqtu?(rB2(f%lc0`G9_RmREs8o7=v0L@|wI68z0pS zmV*8wYDn4>-L?W-*WXWvEbRg zYYtSDoVCA+^!C3sC^&tOw+ft|;1k~gjAX#rpM8YKx$9?*U}dJXSF-FFn?|pPt3L~u zXF@&;UC=V1nmQeEs9m={E!`kV_;f41UYF$H!U7AcolE;qJk@Xm>XDuDalW?HEyY0| z9r-t&@3}9zL;b+Kly7OlT4hwj3A-#K{d2DrXl!^1%&J3+sA_AGxFFw$-_@%5^5BjE z9}ltu<*jl!!rzJeqQqcOJL?Y-&VUaYcj67*eg&N)14Aygz=lILk|0N>y@2Zpcxns& zn$hh~Izu`;0~^;;D}DVRr}12i*JZ4#cn-&?&#ZCFti*tS>YrO&)@Lw5{ouFbcArE* zD~BaE75yOPsZQrZP{0JfkJoc)0QgC5%*2C%8o!xZxaAelib zsK5&o2w{f9B>a_?l@*+Opuym=*6{uF4Xzpz2!e!E1`x{My?2iY%u9e0$X_vQ@qLiI z^-jwo>oEB0BfU>h^^7{@>#X;LId4e&(Rck}q z-mXyBzM&8-_@t-hE1@8#rZ$jC_5^K!pHuX%!N(h-gp1*dTh*w{&sltXzQ7Z=AsMb%_C2*MFUW0{XaJC8#BG4;b9dAAUw+g5((vbjxLlBz;ZJw)9q^xilE4|H` zn7D1@r;HjWTf`dU`iI3&!{3dE^ct3%mrHU-S4#ME20QU36wu@uA1UeEmp|kjDg$13145ZDO}2B`O9$UKaa6dk(KS$K@!_PV&4klBFQ0) zba=Ql9t8s<2)X67Do-JMhYz~`Jtp8g!LFJttwI?Yos`r7+Z;a3u8vRcZCBS*(1-(E z6#s8a0J0Zh&%-wBfGLZwrBG3Qfhh$>PSn7_;I6CYjW0eGc5heENT>3f@wkT2R)y|f zqKWn;&l`$L*RdP9s1x@@8xEqpMrar{z?cCrzn-3)9KvphFN7N!8IdX`W&$O}<=K&{ znp!Qm4gs$ReP&<-iTC+&56rcPwJ9NC1{EQk+0WV7AmZ>6ycf`jk3oDP{1P)iphYtP zb7*xna*!oVk$Or+wY0Vd1(EElR}(PF6b5|R+1b4@1z#V$?go!jc1RNida4d;2fc2z1Vk;d%;aZfy#Rm=IOL7>b#(HboQ|Hx zCimUi8`qkKC8(3WjlywG@Ky!Rg+-K`PalY``)`+jy27Hn6CNOCms6PZ)NvdV+qS>n z1+%xq+qZvhvC)D3tM_zss|F_nBtY$5v(uuye*Kz&fIxo{1}5s1T0o6oTwDzEM^VHj zCGDe)Y9X}&ay?x_!G;7SrZeP9?D1|waS87QX@i7i@hAxafs=XAojPxfesvxy{CiKD zFsEshKd0&D-w(KPaay$$(BKW90Rl!M)LY0T)x6HtaU<6BB0exrRFHF7o0=+7rd(`rm%iyiDi ze8velh64%`r<&W^FbMk$Ss~+KEbo-#5MH-+Xp#Q0r57k-z`q+q!7#|d6rLXAlD zwo;a`q+~1bY#|UKzZ3Tf1|7ol=;`j3wFwChhNM@fX|YkbiXE-BNvos4G2hkEL09BN z^rcBzMMXtfIs5HD$hCmm7x?1HSw1ab8NuofCvg5@sq>ty{D(27sj2|!k7G9KO2Q$6 zZwZ4(Z5vGk*?B~z^EEd%3W2Kt@>yx`H*FvM64yX&f;Iqr;}2Upli?n=1X_AeT2F5p zz^M-(P~N=$09GjYRg0l=1kjv`Z5hr`XwD2SkaOAGVT(dP0fXjs`Pl^o-~iGS5g`{4 zNN{)dMZSx5Y&k1QZSd4NIkaD6AdDv1csBRnSmzK+(7C=xHNlWjeKRs5I-sUry=bo! z2?Idq`umkY9n-m;m7VRn(27a_dsG`T!I|{T^ZPUQ47oqO#^D#kdA2J$b;@|+*T{We zj(^C3_ZF-mxhW}ZVD(E?;NfvOdZI`= zH(_!iUEuIAR(o{{Q_n3~?@qhi-w1T&-J_!f$byAIB0Ox2__@uGdh8uG$BV$=DB2$E z?-z^wqaEqY&B-YMDlCXpgD$}i9EDyZ&Q} z{R*jkkL$eiv@W&pEe{X!|GyDcB)ddQy?*+G3r{NN^LOp`bP=k8`CSuT{iY-2}P&;PxymE~bBwjX)$tL_~md@p1t=yapg=u0FucZSmrIjMOC71pvvP>NOaA#TYEVloyP0f8U?D#YEZK(k;(#n**ZuZ& zbbBA-5C>lat|U0fEZ?v5KdpphdN^$0j#u*M?gmdf3`L9+c`Y)?X*0eHo~eq8FSze{787B0C(acn$W{Od`{tId=T*Y!c@P5lah~~eMN`-3)NwoMSIr1 zYyp1YPjmFvefdyDIvD5y%+LuikWj^}ZftZh|0}p{ZM^}^70kCOI~3Iy4`clhb@u=8 z^d8_?_wV~S6zNOsx!;oJ2^fh->RH(|5$DEy<5b#Ivl3=ic?LhaTEd6o@}?IJ=+l$m!hZ1QIDCT4w*^LDW8lA z_614_wGz@b_NVdi@#VQ|h@kq^*4B2Gandi%@3VDxi*cr=-h2J)I4ji~EBU*m+T)rP zIXxejsr<5)lt>$2B5n{Hix=h8uaWUQ#QY)!#+OU!*fCcxjeud-LpPT3_|YSK2-^bh zAGiHEBSGup(iswB_P&O@K}K}K91sho$(hhL5#$fK%~b&c%o?Moo`K(5<*Dw>g= z{%iQzZ)lGi)RIXc6mY>t9gOdj9nsOhWNKzc7=A)u15oBf(6)SYkO}suVC^tC2D1vg z9ylzJ%&G|~&#|#c9mwnY>ck>{y=7CnNx-N~TS@W2NM}fWe}Cx3pcO8vBFRJ1w8{sy ze$?n0t2;OhY3LcMveQ~*Pg!hEy|xm$Szk`Utnr+4Z1=8RS|^Qc@*Y2aoR)STbIx#S zk1VJVX8M6U4|?eR`92F?WKS^(ohT7M+9)p6D1H?GkdZ`57SJja zo}1WESQVdMRZ;lgUlobMyT3F~Y|5;kq+4x0-MF+-NpzHRPZ?$`k)MoM9QLJF(j z9P4<<^CUN8%-c11xaFFa;seqB^irw$t*1!NXf|nw(D0HY)8*#oYF)jGGV{ib1BBHMYMd^u)DXU#Hmlgu5Mq8-YIv6$)=lcipOkn7v~hDoo^PhojGHD zmE!numxG#Q1vLc7+&Ln-q`&WgjHt=KxPZSoVLC%zrKeHY5|1`TdNI5P#&{&Tl+ck~^M^Iav#s8FCCg8H4xX8>9W%X;tI!zuPmYHtXR9gq|2`nD z8Pnx!(Gz(tH}<=_2|mj!7FNHbQY1Yjzl|VkXWwVadaA?eb)@74!TJ9`QHL+Y_b+pXA5-8Yh*WK?zfZbiisYEqmfC@~r#6T2bH zar9^<+D*dNSvZpzoydRs^cN)J-(O{{&AjD9lR|_~f%tV6<;Ltd_7`YtYGwgb0O56V5DRW>}r4%|aUSE1EeP$4bns3hhh%WBUL9t&C|OvR2OT2%z(O}i>4=2(*RKR$_Si7~OL@^#`Z{>4`{;DyLk91iBjVHEMLZwq zM|kUbO9q8$ue5)%bBl`)={R%Hfk)(-AYb=sB@NoT-hQKS|A5Ag9O+CgUSW6XA5_!= z<9~jXB8WaaFVt(^y}UyvD)P$sLoFv=*Y+Pb!di61Ue!d0fOea52=p-Ev>vdIuDPHZQ z_`BXQDHCzyxOnEj=jM`8e-WWbFP@OUEk0ONxgo-t-MT;(e)jOYWx>W1AvcXlS*%F* z3b`M1V4pV0ia(ECvr{h!^zu0wX=JRe8 ztM*0FDW!?)4TI<`F}52mH|;x#UJp0s-^wg~b1+U4CK)tv6)N}!C=F}|#|!)cf#F*d z%B#TCw6srz5su664ql!GF>`$8=&zB`y8>Ta&s>(PE8`-HXmvE5`(XR0AHTN0pqPEE zxwBqoo$hwO)x$@@yYq|M+bDF>b3Dwet{M$&-1{6hSC*TE8)sJz zO=;>)NAiYygu3fRXkpBQ0wOKQ}aoQxM&$a`zm*qeY+9-3GGy(wRE7 zeo61F@sAxN6M9Oufp7}acQIx@pPozStew`>7kKU9&L$t?a)PYMy1UrFp7zl08g<}9 zA&j5dLyqJGsLpj19Nd*#d7jFo(+eFR$|f_8p1<0d!iAeZ$zE;$@Ve)<53QGd zt71j8b+Wt!k4j3yz3T=N4xO!Kqc33rZ|+NrrH#$a6UUE}?AfzG$)e!90i5i z{}v`1M^2KGko=h+mVwZ4UG(22s`;(&!Tmq;7cx^X)GXB33u^Ub2&FQJH56(Ik^ehP zcXy1BUvpG0V?$UzDj1Dp$l~)`q)zq&#>ywPsmg9O@>lT}{WBbuOM2+O$9y*JpOVrl86pt=dRaXPvHOU$~e}NEos^<*Hp1ZyH^l1ZK&UD0ib93NNciE&0jZ@h8 z_yCnPIP;ZHm8v!k4!(VMl7loybK009^7ic;Zf?sfD*!ILU%evXh6lU7{Sf^81q7a; zrZkcE(*Js8h%$kOm%%WC5n_`%bS#WtDj-Zn%euI2+CZK0Msz)$z8(gUcVji8e&>$p zKk3P7YSW7Bu}bfrbvZ$0DwuA1IGwHchd6hR+eF9h{~nn_hNS_aV&)#9)Tn8ZX-ejq3P}%-$z@b42P*#Z`8ucOzoi#wB^Nz3xYPG+OEQpNf`#hZ zwQFi>owc>KTU!-k;sbUc=P>izn2<1lL$gj2!y++>SYKCHH*}JzQJi00L_P_~gsZ1# z;PN+}4QmmzsvDl32eAI2#DYKdRrrn&K#ho|?jBnA(r||xEPFO@_7{u~rGsrm!vYv{ z`NyeUgwl>6smB6`@$(66CX|+`p>bDo<%%7T2nk)X|5xGe;P2m%oqb@Z7; z;EaNP{OOiYFGV`qOVwg2mE(&fZ)HpVHE+w2_$l%rhx_MNhuSV({)wE*^X)cyE`R8p zEj7#vPc4d=k4i{$EqwSa=s%c=(H6|h*st*tq31x=P-Wdt8c5e(K9G=6 zw$S&!dqno~{bJ>M%fCbozuap&EUw)r|Lbx-`N+WO*6yVpPEuh8ooc(m!o(8=9KLrG z57K!`t0cIbV99Rvrnz&WDdFIHyhCmtIY;q$KTFBVxxVJnL~iRR71?=}=Z~5S%Ljxa zXwN-A#lKsPe}z5RI?vjf97NA|)rzJ2@-m^LKlFHsR6oXSgj>_~t>43g_XaD|E(UK-AfaCpZwoTC-=o5~@(khk zr|<*guFx1W4gItB|5Ps-jKnv8?Xthn9MiGq=Hx383IAh1_bM-@C=S49iO~GHx^^C2 zW~J%^lLe4i>zcJtQCZmy^nyI6OgBGN8c+yaz2NVHibsm5zgeCXJAy5!XFd+`t9&?ct2K-D#G zC4c9X#k4?AulU>N#2m%qEfgZ5$1AnE=h_R3PtIJ8B9GX;^P$$4_U`YHp!=<;{@+%L zh2K?=Hr`Uk(iJFQT~S!ODtZ!XWLOXeY*Li)1pdSBt(mE*mAN6IKw`+W<^9*o&?g_h zqf7X{Cc5~(ig;D1<6*CBia6%TL&}#O;u6T?VdFnws|+=HdvDH{I6Oc|y&d065H9)xuNff0BhqyNCzGePbFJp(p<^YhNn z&o?$Sgr*&*y;=8%kaBPR?@_Gk_>TW*0@0^&;I#8ccRvggmLq1HjU4Bi!f8WYjBHEP$sY?D8fhv>8F ztv@Q0jOtSXs8;X|D@&_S>+HH@Q2sGYOn@elstcRG=B`FIvD z0Mx_C3ogl9VgY{w-Tr=hsFqM0Ksxxy2x)zKDlqNbA+U?T@6hS{_2d5MRHkJ|)aR&W z+1Zjf8yjCx*(Yn-+Dg&*>cK&wpIdv>pXkT;e`08AxG?gkZ76#E!cw2lr(@OPj{08ONULvUf?9402g-W&?|@SeG<6;tOYh^?mBfJNAvIO#Uu zHwY&byng(mXnrvs>+*#qR^UOud6t4+`>$!gnPXb$yI^&M)Ia_FioOA9+?NOE9G+RE z+j8dQz;ON^%i^&D69Tsj&7gKoR(R zYwzc+2_@$V*+<6TeT=KS{qEcTc01ht;1VB|Bh_A^ zRHrD(%gSUXHoKfJbj1p-XCKsSFrug8ify8XIi-t(DmTJ7&^y1ab&2L+;PqT5JY#-c zh%p~)|dls&3X4(2WkB?7L; zItkG7uMqF7oWB?M4#Y5A)>mM-hH@teA5T-${>wh_d(b5K14PHnJbFBM>p%Rvej7~;x^wha?F1Ub%@>amkk$2!uF%k<&C4|06?Ef1jwm<%L_~S>AIMyi9QR3+vi(c`^D#&ao63b1C z$H%R}=_oB>uRNCAP9jtY{u$WEA-jP3VQy|N>2vfT1EC>tmj+UUkmXIcdr{8Y3`Q8FWoI`bg+dpDiYAdBIaSKKEfi+HWa}48 z)#d$GixKEMpNX@?APntiJ4SX&Rdvr%ffpfcU5EhQGV?O+C?f*{tOYi3@R2647cnuv{mub} zzet?U&dw+oJJ%HO3Q_hUKTdxC{^I%;Y--)y+@RQ*nzB>tw5-bpf4_wLjSzdW=4x8- zCs)}_M2C`#^);E!i}K(&aa}S%e8;YA@P^WyyZxpmiv|zz#GZoZMyxquijX|CQe?58 z3Mbd&Os08SYF+C~?&Zu|^fNM{*-G4M_&~dQp85XMn#w_f;kO81Hv4*4B~R8_~py9Fn+?$jK`j<|GHnlem^iEBw}#+CPU8#fSigkN-yQAp)DD&0tMjb!~VRPY28b zv%EQJ2z-rWz^{w~>jIQYPEP)F$LsszTF{5bIzmJ+wLW_mE(gx|XmJS%^!_)L$qN3h z7>y5->R!|9?y5XILXDnKu|sJg`X=!r+3gp}8hQy4StK0ARz;$9{^SLJC@2;d4m`E! zsF&H*S(In1u0pnl*q9h`^BCvnB0&Aw_W-4pl{70(ea@Vp#7&_XFx777`(p&(hM;2>iHt z2Nc)#kNTyjm{gFRFf@2ow_az#L7gq%mZ0JImIAox^ZJkJLo|?T=|_BT$9@$MmA!q; zjXn?pL)M4Y$}(35^V)(Mf8&tmEag|25CQ!g((S8z=LNSJ2Cz(0&~O&urrjKmLOUFEsjA zuQR3Z*PcsVm%XsL|JlBL zE>C3G$^H}?r=_KARA@#q=|1d*eF> z7=WvLO|+5x;_n|B837Le`(t??!WNDoTpJ4mzZpjGhsNz<+TtZ7$=3Bm?gi)b|0 zY`CXh=$-{Ra4#_d`U$R3ppYQ5Mc?KinFhhC8BFJZYwJlqkrU<#WnfM+GmL9w4u84j zs-it)o%8U;1(q*egnqG~Wgc6h=N`)yw#U~AOz(BR*j~Lu!z`rdAEM48O+g)!Q_MU0 z^0Ws+ybVxbP~rB9LIMKcv1}R3w9-knZ|CD&Tpg|ZgF*&0|8!_iU*FE^@O8Y1CpT~2 z{OBt50(2VEhgseHspDALR_Qz{*U>roQ*)#)ydmoIYEhwehiMGkUXNG`UR1k=51U5p z$G?C7{_EH4fUDNlZ);s_mDtLFO~2^ubTQ|`RdSI*sRo%E`qNP;lMX+ho|% zKC8i9XZ8wxJg@w0Nk;ti>X)OO&vZFV6lW@Ifkn_47FVxYv!O zkpNJy1VB-R3yS7K+uXc9{e{|}z%c~1jI=Zi#BC!%F_IP)6=5B+9d(^=YtO|c0?q@N}&x@Ax4kUc-9IktWLqk~tPJJ8V0WL-V zT8|&eAtCK()xea$&Q>sgEqYbj4>cS%F2CvR#YF*=YkBHaFpy8YB>-NZf$KocSw4N1 zjOR~W=CQwKRxgM~I5{^RFTAW1?P)yY#_&+cg?fWdghcQ0X~n#bua>9Xc+qMWOAX|Tt2x({hh5FnP&L~7hj)a+3+b;d={I${j_q&@>4#UH9z6Y z=>*Jb>$RZ;*t?Gnb;HVZA9LWp09@M|3>~4ag;oKT-eL{d8O$DG&CN5Qp<)&d`GMaC zQ-W!f_oSHL-aA2noAY{Sw1Y_)-C6Wrp6wpf4In>#jgc*;hIbzmOvx zyUFwi{P0lq_us;-qgFAK=>+%5X?(*)ld_f9;*i` zmt=L-oXr3CeN`1o+IN*zSN~aCJL$7D1(9|i)eMS^`}e=x+`ZS~P4>mf0w_eWxj;ID z6MH8cRL!OSiu}H=^DPHDFc7soxr}%v#=(kNVy^XX#vphW{;4&fr|YZSG*{fUmwtI1Z>4Nw>(!h+wN`rz#Z*&88j zHh3?d+}SdiEI{QA%l`95Mp7WyGi#J~Ln9-ZCZPluCL}e)0!xdJm=qxXiH-;=@Xm+;v?{n1MDX|Oe_@Z(?ppesl?(Iw z8I>i=S~=Rv;r26?lZ{b*-R-ersX@DAchvIc*XIRA15~x{IKJcmqqpRhDI4`RWZ^q+ zgSVLr?faLzqIWust_yEQN0oWJMuYVNAH?A$OQ70EAj2oIuL>5HD+Td!pWmQ3Yj6LG zSc!oJE6I((BwbBS!bT}JsVh~OsC)VH4BocC1Cwdxf$ycZD|@<^FYcuxil;08Z5iX< zGSRqxZnYx_XE9!$i|N$Wt3ToKh0-XexcI7*(=b97b||Cx;TI4<^dD{TsRUCwavD8u zTpSGvN!ZGY`>%h8IH(EY8$i-mrO}XmeE;4Ug*dd=_Z0(d%1tqogC$X!H8nJcwBGsy zW-3UR{NA4YeA3uavO#5n{+8dw>78>)LyZqp9WNOk)2s<0f5^WfcZhZ$BzRxO=hWou zOcslIiT=|MwrjLMj>Spfb%7}hWG@8NV*_~o3S+JkvmPDcLyj^dS^&LV;&E+u6R z6(HgqpS*nLW;+`@Mm^rz5?-}Q)fUipr~tlCO}QgWPS$^U?sf^|1d#9ab$1hCUSO!| z@&Hf;^d{;GcHZMK%-bT+jpHq*4BBOql!c4nO))MuZtFy$K7D zXPwVZ7tEA@Gs(p;!gGhW2R}hXEwrf+I%unO-{lXl|1WNn;4Y>!hvYn4%ihj>B-y&| z#b5megajhQ#fBrsE`Smtc6Rabm|iOfrv4qW!PlLg(@@Jz2R)a@^aQ8}3=$k67Yj1x zfg<1~c_?|At4IBvR9VIv_vuJnSp&|R_ql}H(NHJNzG|Uv3pr z@xD^kp#ONjWsGhaVca{w51BO*gqnqCWI@Jg{bwq$3UIeI0Y#7{(0X(lDRY zBlZC50dz-b)EO=rhte#1FMo@Tj_!j5t8f2NX=w}0f}l=5m?Ly7BIJ9Z?DU7hszAHISv7vE)W9G`F?*8woDHpdd)v-=hE*zs!2<$;;|qfYxB;@z?RE0>k7r*i<-6uXHsLfLwJbo z9dY$A<3N~mpi@3QbQ`a4WKDQvpOD$Oka}66*=6I;;3l9MrV#1kD=-yaS7=F!_Fk@- zEPX(fnAm@vCOr05S{UK-Vc@RD@hz{6j7%VLa&j^d1;FgIcwTXAD?dLSMd^#Z5lihj zTmM<~XUnJ6>$_($)808MFg_pIc8h|yx=O-#S9olAMTMy(-@}Jo$Gzz(PyFRp?*PsA ze@`#>X*<)}k#fg)tYGYZI@70?hDnN^s{BL4T{LIZjYxT_b@h3HBmV)8d|0nR&6bLb zWRqAgf0ObOJyCE<$_KnCbd|mS$vt+MJqW@zjW08l<9Qg5=^O-$-=p;=fvL}*Z~RZD z!~c*iBQ==v$&G)K6_rA_4fVUq&aoUNHj}gIi5YZa_3_@^S8B5;$5d)Vv%Q-mxH9MQ z0PmmuL)K0QhOEhX9$x7MK385}IevS|Vk^MPxl7Q(8s_8d7gio}Gk%X6&;t zezUT28E|0l{>q99GcfZ=IVcJ{5>g%QAAVgTxgZyv$i=AI+l}dfGpSSwq=e7S{5;H? zIpRaPH)n`R`Fgyedbm~m``oq&dbXy?K~2=zA%=q8)|3*>?s&n z-1{2`R9R-OzGs}BoyA!b6M&XvcW_jc*2dTT!a_9i)fD6h?IxWM6T)~@ZHyocyWiKT zCD_%=gwj=OeJ!nN5FZ%0u_^cTqOyD9sYP`VofIHCbuLy#4sljb8ydesmOv0Xz-Y93 zayLK};E1?sLPw579+!|vh^f>(^+bOKJq(WdeR&`KKvO`Ih{Gq43mv4GDht&v9Th{9 zKash&sneLI$1c}T4CUm2gH2DdbH3V>3em$>6Slk-FUCEDEjlM{cTot|8Cz7#UG?dy>i`-SOe)y7PEx22u~o`MyV@=X%PZHdt3rS{r7kt=o7qse4f|VqPWhq z{D{FrWC1wGkpiGcZjqe(d*XH`hk8tF|GnDZAp^h2$S2u*9KPNZJFuRsbK*>9)M{2o zDt|%IM-G2Y!NR(xJFT0tQ-1ikoezp!J~`!41!aQ zBOL1^tx3^j+roMc$pRHKbUpm=ik#%Nb>e&Br?(7 zDN4cYsU*paT&vXs4|6K^*k6s@tdu2jme6`(hS3IO2&5O%j{W0+4AF%#y4i%r$z1{% z0>~P395-BCd^T4_n#)kl-DQ!;1zCVh3O0ofVJ9o=l}$Og50V9(<;D|H z?bIVNFtgQix*_Y$Y;sI532{STc-QF>5 zmqYid{b9Yc?eVK}>l3-WE~|TfjVLQAe(An0v3`mE4p&?LB8vp6;L3>_i_^VrKTI2< zjtDjw`|JyU$5xnEBJr6mCyVxK>;vO@#)s(`n?^HF$BsM3#mG24GlNyo%s^CuqJ!-a zJbc)2%}P~&aHZ4=Me@S~R^t7iX-f8$=boroS`^xGYly9;9UWQhOx)uF{w72al}yO*}nUu{w_cGJZRHy|1GK z{(F453-mZOG~_M+v3QCY7DLpnudVg}yAo+R1(#q_Y7R_VYM7Xq`1$#bt>VLC(cb3z zdScLnpFgic9Byg(1w2Pxef{v~&lK~Q@+AjIP0N`EzK^$E1{!)k&7aR8aJ*YF)3ZJ9 z9?K6hP6Y`E%?=(B=Ih%hzy2|z;x!19uDD5o2UJ4)nKiG3*1ei#AsbC zbp6o2Lz7dPxeiiRi}A{F4&A`OrVIApB289lh)jiS6TlI?zwrPoj zas#{wxpuTAEnI{Oh>3{}L-|7u_a^jkNL?8OR0)s?i2i^pFKU7@!cfP>`muVi{Vf;@ z!l^Z|rmd-k499c!-8AN+hIGtKt2`DqmAAauE5EB$j?{NlR=WH;*hST)&aCRsXcDM; zuwrR~IWp_4#NsZt9m?3|x8G&ASK1!k9TI7~B8W><%ick>e`Y!L>_n>YRUiN+?Q=^AtizyMB05uPe(_`$Y=<%4?iEDMPA@+n7w#XQi2g( zM_b#Y@Ong%zqBI=pdDCfb6Q4*n;U=^f{Rb)FKa(%SR z?Tl>?ehW)`(H7y|aOcR}3R@}})6Tb+q+H$z>F|@68qzOE<+*(zNUNwhDaPTxj#nru z@DBS8hK3khfJ|VMUEgqaN(!Mr#3gxGkAwCN1b~(ne_t2aLBh|kTrTJUh8?!oV={j&m~EZ6|TMh$l9tA`gcy@}uBF2)_BuH&DN2n%mv-7ol7 zMaTXJ@9YVCoG{I6prcdLv5Esu%`Po0B~|VI89fv$#@rS~a9rRM4++BGg^3Vo0Q5K?@ zp+ITT{-;xR!@fM@oG5QX>c@kmwgrw{cK>k`+RWht)z(?`yfy$!Ga$o)rlUC+dIv&_ z0?;n=JZ8RkLwKQ~@d8zDHn>B?ysgjryS=x!e3I_A*_0z!EMk<86c-U`7s_nC|>>O8-Un)A*^WYfC<&P{6 z5ad}A!_sGh&qtr##v0&SkB_JXm`mGr(~m`-SgwMsQ6-#e7I{)nuOCGy!oaWJzkkon zK>IK~>Gd9?{S|=k0ePWV1A@T{r9XNx>}`ZRc4B(k1MmY*7?h`G(B<_;jMw=rKQ1i= zjbn?WQTX)fHuU)@KE6*+|HH5}zB67Vwjw9x<=G;~K`?fHbfET_)BM;Mwz`#Jlgc&Bi5A0xRkDuS>)|NkLJ8=TTTta2c`#Ee?Je887%@p_iC}00R9XX0<##oc@F|r0+LxcT_Cyh)Z=! z+T$+Ge|t}6@9jj?uT9pcdkT8V&X9g)VL}mPM{&L&%BvtN)J?jway)?DM3lGqmU({E z%#6(f85Jw>wX7?gDGUY~mT0A(JAHUMT5h{H+Gur6ZTIdFEce_8&&HRLLuS<*SL3yE z49-(BGBNpjdv76dSTpa-&U391y41z6SQ%URHx3r-g=0h1Fadx<(SC;>-vR6=s9<7LBNdLiTba9w1@42YmmA?bU zn0dEza9Euk_zrY_LWC7!k{iOmP_z402{@(Q0mzS3z zmqfVjwv)vAS|dY4Z$Fps?YrWVlEA81S4= z`1w7MyMT5tc}B*@&iCi2V1BTsW)-J5Ha7P8@N;}CK|< zqeeW%+fRus@$Q1>uQo)9q#xYmWeYQ^OVd8jr0|N+hH4*s&*T}EdoMH7A2$!32I=0t z%nS^8Oe}0{P&!-Vs6a5C#Bx}8kBL~@59tc+F%~0I(%siiSbcPKpC>Lz-!2n#xiU|= z4Dk!13R37d=JA=CTE8`Cq#PFqqrRcfm70Rly7nbTdV1p+4aP&u&!_nH{#iu@l&yLQ z4ceKn(1C-`Crr6@lr$MUKI7u$#jMn;%(%k(WOQ*{blp92L-PyK*F)C*;@#S>+g7T# z!dUO=4_+Ggpc?a6F?)I=++MO@b?xYp%8!1Ru;R~0>`qEbN{El2nw}=c$R13Bh+9DVVWGMyucCrNBa-Xew{MZ) zrahvc{^arY_&Dja1Tx#T@9~@aCx7FGEIuLRSWxan=+1u(6)|5wQA%BpXaPJjk!1l1 z3Fy(wo}SrHmEZ9PKW9gt`~sOE5#&eab?%oVFG8RGoRW&bfnhh_r1R8}HNnxu zekT(gBaJ@OkwbFn5Aw+0ud17g{t9asU>M^e+dLPkB)ROeOz9j*985ZgMtd_=CC@m} zIx;%i&BMdP%j*qhgBA%E?3e<^S*r(DG&ZzZ!8rAG`yz_<{lE9f6&A9QQnmJ<6PGBl z8GA_G+t-I*I$Cp$eeHzGWuSdHB9Hz{OHon)bcI9%l$y z6p(n(%t62s^X#Iru_u@?%n5?n6+H0!`*(r1hOkF21`t}NI%fU}{=RQ2W$W~Ufj@_* zbJ(zT3|j=PZO&uSrh`9XYh!X1caMbc*4%aBGt-{k;se507CyfxyGE76E0B55X}2?L z;)f4XJUlly-&H<)CI%~Zj78$XWv8XhVlqC9-govL|1Sf`J4HE5@zvPaVE)vy9^`dL zkmJ@9sD>VW1+9p1o0OPXo76vP2FH*9!s>8eY|HJ$H%J;A19Xhsi?9B7W8=}|$Av{j z%sUc3NA04W1|G-eIsK!WWmYF}Exn_LmCW+DJS=xjXGY?iS$!-cK7qsAzA{o5cKfTqeaTY!We7#3oEaOvwL>xJjH&ukYZJbEm?h}ia$iYP8KR;FC7gw&#wTl%6SAA1pFHJjrlDsw)^_r1r7l@VyDPH8;JZyy4H z@jOdKmg6-(mevcz94Ytl>WFV+1$n4Qt*y^}wAX?BHXiSESSi5g*wYvh)U3{q zynM@&8|!R@MsFn-p)7*IGV(HnqJR=eP)Y6mWq@)OKepb9W>r3Ze5lkwDeDwL(8}|W z`sjy_OzPAD;;^gocSR8p1#9Sv`YS$q)5bEtH4Wk_be*|oVVEFQn&Ve`Uidw(&3+D9 z?$b9=WdadKg)#@4zp<18UB|`*fs-;@REQy(Vzm0e1uCL2=1rDr1>`L#A25OR3JAd5 z%nZTK1H`zEZVZib??C|p88D;}(}1b9w_7MtbCu-~1Nd_NH}d{Jun7>I5)BRx!s|-Q zP)73T(I4uZ$=U{#mEUY9k6ow_rF^oe5P99snOZcvHHmU&n2Y{#OYzS+qiYKoX~QD0 zxx2QPE(s*cT~$A@)r_a_%@|#JlDk+vxlRj((er7OfTSp|dsK^yi$va|RkcBTZV;T! zUfeaQ<@>FK&%7~oY-9fLI*)g6>L;Saf!9@Ax=$J)ej<1sZU(PuN2X9c6Ye{!rKN=m zWUG} zJNh;BLq7DTc$3L`_+e-${Diu%6Y@=q1~_BXgHRx+KXh9_#zjxRE%XSB0usS8@7=EI1mEe%3YS(0WChJbr|ZD$-wc*<^XQwoX%t^YRkzj#spiG8PEX!e!pIl) zrvU{V3-IAbZ3z4t9-ig@owLwixL|kg+(Cs?@_E>4o}Vv7sa?YA@PU&c0`@G;i|htx z&GmKGwyKO!JkQ>#llIfCr7$Hs-ST7SjrYIA;9G(v+h*zU!e;DSnTuBnjNa@#CLTva zIkI=eqfzgw4Z%tU!>lB%9W5=~D2lncgWD&_B-bkOIPSsoC-CgzU!~c(Iae;c7!3^# zb5qkjq@+=N&(ks2iszvkFZEvC_S(xIxBZ#2)fjc+|Lhn?_E6(qMs_5emfciVA#QvX z#k79q@Tpgo`T)b2L@${7*2l!WM94$@DJm%u;p1~d@WafqivTmjGeLQWs_>|%R6B2^ zeMwm)cZEc#7-;85Ce%$K1*0~B`;2`r^4`aXOP~*$v9wRNCg%GgKij{c}u3iW*x%W zbg5m2VGL-c8r7FaWxbf2-%*@sK1*cvXrCYD$Md-+d=tV>ei)8JPeI2H$%YTcfMCX^ zrm9Nh__1sP4ZQIk_M?|SuL#^b*U2|N~PpPE&VqkDweZs+JY?o5{# z3S4ET{(O9G<*lq5s}TI6Ngz>^a5!>^gIoA{{+pH`EbBok4xfJeB^`PdTK(&d@oten zJVN}L_xR5u34?U56)M60S3`C6@cL2U^CKhj*vAC69bx}b{XXFIu44JnHp0M$m<2jf zfMCR#n2ST<%s=_*>M8_#?O1Utea#oJS|O*hD)>fNKB6$!zIyh|8UK|T+F*2k>?{l- zk7dC5qk4e072<4y1a$>UmdhCJ!LKnh^YBwA`Cqd4)i9^EHMhB{O+agut%7^-G|=cb^G&#T917`&;9x6>C3)zWy;>7 zwUxLcKNuEp_w1icS(jUz<5;{xgi&MdJS#hjIpIDN z2YWKCQT1B=0nyz)Dk@C!Vg8O_JA_(lauGtr?W+`f~Sm)G3f92rS|oie(U zO~#pH?ThDP^cP6^-wzIMAyol|gP!K4d3Q8sU1~9Z2|5ZfKqP3f{PTZAD@YoL5=y6w z!arjgl;tCZ3wxTPHJHkiG(xA#Q40K$j*qkeK5HACc8JoR zqL228j&W0f4+?58qQRk|6yfudu(d>hfztcZJivN?*-}R=qq#^;WdZ1Gv20*&K8)O|t@%&#nm1}QAU_)@W`MH1 z8yLWTj<-c7VnopGjMQAqNJ(KP5)cru2s2btn!pPJ&thBo36K-)n^sX?gARq7(F>6l z|4syEoiG8Dw4aB!rltnJMO9^G20EQ!;@uDl5L{1yV&g>ZhFi`e{z#uzBuj!1a^evK zh*J1JTL=B;zjK zTX{qRy85WhW4;-A&yGv4sn{IyZyV>_bziT;sEm7AEWXl3k?8wwq6Y=KP#_$LePDR* z__5>1pU)LAC*&6GUt2CKqw954W$4&w+uGWw#83smbW8=ah>znDs;gH|)_Yx4ZFb-% z&FZatgi;cp@<@|Cc$^VAVFLR}5THX0iJ4l2QtQR%nwG}Ks}R~;8`hdBOqyd$T|B{& z1+w_{ePx?nU%DGETu{90l+r(Z;10Gf^QkT}J6sVR>tK529pG=7sv)6wb_ZHC+;uLv>;aO37~99mXA!H@9(J_(0!9IGw+8)L z;MRIMIx)zBn1bHFd$$D6zjMD~n96Nspgq{5{d;Pv3L<)x6HGz9A3v^R+PR`xH#HT} z5eB%R*>C{?j~m$>L;HhL92JG>;N@sG4^09Te_FGy_eND~0Ua3JBSkBW19Oduw(G-dDJy_dEuEiA@>YR}HkUvqFU{>F`1OOUO*xEwirm?ofCN>z#0 zR$#JiU$LuNd&SOys4~-q#n1s$;k4*y{uv){?~4xUx4W=&OXTjjH?i@F2OBbf54_O2 zzO_akd47oPf;R0x;qp3xr@E>yDf~C-KOC~w5tB7NM_Xft8XMAmBmopnwCwC?*aHJc zO&~tc#=1e@KUW~@oE#=-P=RSo_cB_XJfDq^d=fHGu)i;NNS@MD|f zQf7F3CT;S^4<}pO_F-OB`qZfx_3Ax^o`w7uqzUV_eq#b!zxVYVu`akSiWh$!-wl1&d!5t<@ zp5_ET0TDzLG7RF-e*zC1oh-ROh`w99aM8Sw?j}cj32hHKoHIXH6_hpgfe8ZKmiYAR zX*PmvaWe6QVpFN?$GYP{yv@&^D_y{g7)AdwX-;MKdG^%`_1`<0uDxF^8(mgY8vDDc zzcy}!74_!C@_c4vmf~=H7%3>VEYSACrX13W+`K%ndU%8kj6xQ&qSy%!AptOXU?{h$ zmRC%B>z;13(5m2JVaUdYz{|~%-N01%a*%>wnn}0ZGxeMiCD48laSx{$0!L?ND zp=~Gjc(upHosU$atGw||;dR5_-HuO91~qH^R2&`2E=5vFRrk6-;-`yvekDos~1Kxs2{0wahrxRVs&8G@)s|8?G zb6G}yS*Vmba9pFT2y~aXe#3w4U9KACy#vY&h>mc$6!va_`y-}H)Kbpw`%kb$ zqb&I96iI@)t?PeetK~7x_hNKDVYlwI5bvU+i3{coj7td*bYvZUx7VpqD@8b2;=v=5 z-F%K$pv(dXnEX-g;>E^yC#mi+@bK^uMzxlgzcTmZUV|ZczivW{BDV=p0C*oX{uK6A zP&7iHOK^jD^-5<@du>oY<9*rX*af{z&xlfHij5%8$f%ZY9!=H9UzA1crH$HK@?0nno(QQqr~_6K!m~mpLL@xwH0*ptoyfImxQ7wibW=W7$ne z4x;fA(aYd?H`qsO>lqu5A({Y_b7T|dQI!PC>Fwu-nW!0LkEG9-B{*knER86$VgLdJ zaP22#IY@m%6YLRCZb}8&e!Gx)GLqfop84uU2BHN8@=dR{uPotryJpDy2L&$;{@evs zgt3Q?^7{U=k8Nk0L&qFyPnb;oe^k8(IM;jsKW;>J_RI_!iL9iuHKaoJ-pMGEkz_?= zM9L@$*_)7=nIeR;DrF{78Bvn)dwe?g^}oLV&vmZ*y3T#hkKhAVQx!RSSRkiiS?z-yC%{#ER?DrXx{@t*o~z%DBA=hE^r z#=u_xYH_bX)+lV?0^lCJnQO=wYzl(119+hJ@_Apd9~?M0uom~3*DQh{Ao&#uo#byw zI*?IzzUP6#x~Xsw3{WUf@L#Qh6Nbx5?R5?7n}f|EpWWAKw^LsReu+zb1)P9%Z|u~@ z1qGTHYn@W^#Bq0wisl}KHfv~PEn37^K47Owc43wC{p{cVrlRS{DYu;z`QorpVxQ4Z z$QZWCnrT;%rI2o40z{Rj$)9Gh)Och!-TaFw^@FC0qU^iAd8l`NBKeirXZsS@;#Kez z`wSgk2*gLUO%Tuxz~v?;4wBr(Sy=>;DNdv}vc_r=8gKgg;Iw;=t`TpBK-@z*EMGr8 zdW!r;Mse{+{ClG3auC^^RKJ9a35*onJ*r4${%UjSW8EOGH&isS95TnImDSzSb8cU# zkg}1!XxUzSH$t*r+2ui{R=R&?Gh0}28V@;VHMPEM)ayiskF8(kmdl1T&siAHKhEX> zub-i_a<7`tsD_fiit&H%Qw?MWfz&o!)tE`~`7PP7cvAnT;R17(h;{54I;EP=J08|# zx?Mkha@V2G6YCUDo86plO-DvVBtwaVi+7ETFewkP$;`~mWOpRJ+VkEQFC5{zPWb5Z z>YgZG6`G9)$SD4L9=?14uh+1a0U=*-cBb0XV|vz-IP-h$$F!E+>EFbI1_w2EUdw4UWxH7*z>!9;SMk4B#xUhR{+mAc<2~g7b{ZOT zB8^vEzAjgmhi0b>`;|279+z_NmXB8w_?kKEDda^I-gXF0FHlW%>91&jnIK(h3YD4A z)rm_u9OQwjL7s=N1uIgT!>?f_)a5ukU=N%dALrXzc`kaOBClM<`tcR($>!!9QYLK; zUxlL%#`E~aXpVl{tZEBFtc7XBVqa+71I>TUeQ1QXDly!VkoDg)*UVDAINDv)B{Q|Z zg1y^Ftj9dS{wn2Ekn-%AqbJ!|SmXem^YZcr5JNQR;ptgXS@}6pX=Z7!h zp#p4|$jr;1|FSKaC!{0!w?JAytZILeSEO_4P+gzi zKEGEVxUL1#VqEuqEoVVpf-+#6=iETx!hx_Qb{oL7{uRBJK89zFChR z4F9}pND+Q?i@foJ^?A@FD+j`J)Y|Lg>O$Hkb|3b3?Ti;f)8Rml@VFvCebCOx(&DFy zwPQ{iIusO$CU8jtAiH(G)`x!-x^Vby(B{a++a!1dC0A}sX15nV6Xq8>t{-j}WG8Uf z%}wCE8(+`-kAnSHwW`Q#(74OZpEYwUpRQ}d9jzyBNzi$ZKvm##_wF8KFhJ{v&*R6o zxH#d_lC>{5*!}w^#hX=U2&Y+NWBWL)T8*%3H7@1Bs>L47 zxyy?-fPeG5pH=OZ0CX|<1tTTPFPDd~a_%}_uT1~LppLGqZE{?b#`3xK=JmW+83ZY| z%_U;$;#Zr-Subkq>j`0rxM8}_V-^H-!o9T>W&veDbEu%vTEW6{ zU-$(9!L53*EepaVGdFi$tdq)nX$B(^3SC23RtiW5}0sYu=m#iB)OA%3Q2j7tN zSQN^-?{vu=9){EdSzp&nJK!S$Ew3ylR!~+peb5otV1E8PJTOER!0J7DHWhst=A%L$ z;BZ?MuElTP8sJ*=FeF9|{|IB1y8uez0fL$3rwczf`3_1PsMT;I|AaS6z?tH1uUg0N zG@)TAPy>$#Xc}EB)Hb9teBWMBqF6R?)Af*#Nk^yMb9Nf;TYr@lzTb}HYrNws&$B+6 zA^E;C3^?%Y4yiSACN;s-Jf)}b?tg~#0u~=ABQ5|#K$}@rS@|k$>;Vu1ZthL&PgI$N zU?qrAAVi1xlt5K-mA~`7!V8-I&$;#%3DcH-N{hN1y0;{GU`isu1Ah+Ox>Ry+q}4io zI!SYj>YXoVSgx-7Qdr%Q^<*}Q#leV66+84Upq2%DNO*f+|C*gDLzaY>7q^@Ig^{HBC(Z{0Cp8KuO~2%2A<&P=|M5!ys|&NmFimaIj*A zRbck7WZ{J{;mkOTRQ|3#k}<-Hno&+B^UbDgtfmHayTygV8aMl^c`c8$UGQ1^`8{C= z(N^iiHfLhY#B8XgcU(M4A09WN7VlAP!0!!t9SP!)jhd0&03Ous{CT6t`nFC^#(%z1 z^2-J3u7^o5!&VB~0DJ(-g@n|sxJQqmE$MZvfa)x>q-1~OigN-Pb>ffr2A4eu>ntnr zRd4w4>4!@ySM15|&>$JQO=EARtHnef&?hTgEvpqN_0|`vzkX;?A7_f3trVlP%{Raz zOXAR?Wt(0hOv_7brk zC3;P!e>e_ad>9g<7G{I>M*!|TXK4PFt2lP$ocNsB2$k}g)-SdO!;8#i8irm5@tRDj zX+lOW7P4y=6-TQXz zHZ%4b0hDp^!_%0*Yn}oGVSQw{Rbh>Dcr>%({bqsyxlE3+$49=i3O5jT29-}Y!CMUd zhS>;AjEn`!Xa0rwJql{UbwCiG;Ks3>sQ^HS(-k17^VeMI<;6o^lWI%u z&*%q!JJl77dkT7j4f0U!_&rfjT&^O_v zaw_=QH?gPw&WPBIz|HpHbqIy(|KBR1MWdLYM$Z%`!&Bo)`iG_GtIoNX zc^>Qs=$xxcPKfS}`Q$k3cJuURTyj`Fc$~1a6par0wsF(0<`m6g7>fA#+&~SYl0-Tq zMdlTw+ijtbQHjtT!3|JXUylOQ;|8^WEp^l)Y5>vVk{@C_JekBkssG`cnU6c5O&@(_v zoAQ_Atgh~zPUx&0k&8pnECM%&e1F3mbua<5eAUY@FYgU|E!1a<@U##Fa%pbg1#=W# zF+2c`KLL6+fzKS{Srh7|h@N?!ZQshW`8 z&9V8W;cNNQWY=?zB#tUY>HUpG0!4Loekhr7kHNzOb;0_a0mpn0A^IOjI1Q5@C{3wu93LkrP5Eb&B8{!Rf0RFuuW-n|AI)-C_|Sf< zjP$&Ylsx2zPvKa&=OpzYHTC=4983bbx?4n9G%$E1%e!$d{PKr2mg-v5@?04q|Kj8E zQyTa^+YS!_hBN}~{y77!-iYhdd-n#=*@9PT{Rko-b%X_C7eVVl&+*1<1Pl_ZWb2t^ zDI07Wg7Af>&9MRq8AQd*TwK(A;^Hpwl|Z9@5&t})qPXh0emE@#%FxkW{M1kjg(?2* zNagI@+=G{=JMi={O#(BVBHC?%RNmSP7MwqHKwt>=)l29;f6E&O(IzkYydw+4K zPyKbObJbHouWKRgrS;=Yf#lVfF`W_OSO3rwN!z+VRd)T7{@buaWb=tQsj##1`P$38 zd4s_z&IRF}8kY{QJZ+n2Q}}#ySh6Msb9e6Yz|$l2zc|@6kN5y5rn82-&8X+*oLK+M*-neofrD4^b3l}Ncr|{ z@qPO+y3!A6PC>YnK@K!2dX1olAaciqDi)hsWRnphu(1*X1GiD1Pkyu@gizbPr2An< zexn>B1cEG(=YFR>_SbyMO->WcX09oc#34!nUAI7+z*!VU+qU-Q7hi z71;LRp!5w5&%mA9-K}4E{rYFZF?cdD$>>VGxvMszRIK~;Tw|}Si)doEb5JmqaP^Dn zp>g*`B_8-v=)$Z7m#N6OgFh^d+mU!532uozj^5{oE_)9Q+8{&|ABEj?O)Okg z)P#@8zC<0xolMv~8hjiaHJ6E#UME)j??{TK`L@%#m08wF5HEp;5Yg}C5cK)u9gCpj zR#BX6{#r%P`D()eAlGLY)6I&vy#}m0>q5M#I6P4zt>u4cNBU#E%D7kf1tEAzzIRWs zL!n{U4*)v_(e1~4sYsaa7&y20to|m>e)21yIY+kcP@66NMeP^Ao25zg)R^e5Tq58r_Py#^;2j>#uP68}|xg70j zF;Gh*mAtzfY`MhGXhQR&(Gkz+F(F&B8U8I&DNa<(w&O%}Dh0O%tQcIPuhN?k!bYs( z>IaIq-e%46{@IG9`wD;P_KpR&sGQn-{DRDaOT|)~_ocHj2EL%#>@w+6J9DPw3ULzH z%xw3dS(>lS_V2PvHMz+{8uuh!Gbq&Oe!6a-T-Fx?(D2$j)7Mu$e~-+aJMNwM$Q*+p zF~7(E?=LqTrkIj7@kJL9$#F zUfX&dv?At^2#p65lGw(_o4=nyx=<3vtkgf0DLWoG<)HPrrRGvj(0uk4`PxiGb;ME8M5U6=eL*MC;OoSi4Xn?cb)OfY&o7z*F5 zMIajx38Z^Pz^h7ehbBo$V1xG4iwTR}dY3h-epNkWE~@O;|NSMzG|@C}v$i2>Bg=bR zn@iaqTNmJx#IwJ!rx29x06J{jsZ3dJ7HZ^D5WUd)y#MU4qm+lM_TJnjOCwQ|B)45#{!sSSSybJYS}oWJmHTBEQ* zp$mxuepw7A%?A08Yzf@7YHDf_Z3BeNIg^TXvTIAz|I}H>(@5Ok)5h`!olQwkqlq~X zAWcp~{871s_b##7o)JzPhh6qFQ>qI88dtvHAGMP47D0#kYnVugjumWQGPkDV?yOUt z1yQo`=m{dcJv}k=LA)mpc&q9(b9%bIOw-kaq5U@FMpV*HMVl4V&c)Q8r%%X;j}$`M z3^6jII+g&&oKjA~+)bdf(8)mJgUBMS4vt~fx;mjt@{tB_nQ;e;SpZIlbI-2a7!@++$T7ZpP>Cl) zIE$4Ci4$c?wm2MmdV3{N+50b;v2!vUYtC>1rdHUq#%erF~@UiI6sUWPz zdEkWY;S3<$;$VPbLq>)lu0kao$LPjJDH+gw0eb?j#18W%*i_#zPeP?aK^uO9?JQ>K zENGS3T9FqDqr~iOOur?Tkg+dH^|VwTC6Pu>D?uN>NZS_sP>Y5!h0Z6i=I@P86&HdV z+k%rh%~?vGn8#&LK#&97cLqv{*vD5!zHo{+Bu1O<=jDryWg4-WSBJh1ajwo?-!_LVSK z;%W2=~&O>GBdUPCKrh8Zk@1bz9^oE05}X?J8(|0Lg*anQ#-mKF0B}N{b>ttucGM zsIGDIW(2eT6|Pe(F)oy5U5{%^a^|e5sst2qzyt*ajX?zi?^||S&+FF^WNZQRz(iG~ zmsqvP!yXT{F z`}^2`kr&0&CbbQDhM zG1SwGPb}e$I!Bm)egIl{ShwaTzcjX4B z;L^TnEt>fIv1Y0@EIWvD<{o;__J*txm6 z8TBr|ie)z}=R@em%Nf3JJ3n3jYjW*$2lFz&EVFenbs)#RWX)PnzLk|w|Ds8V5m34+ zL(&Ak7F1$K=9D87t3Asnvrk-*-7`ZtjT~S(=N71q6?6RGN6Zw42hH4^ zfZ#L+ZoiH>1f?9bu{d0!0*x6p;DE+`T^kD%(x=ShyCybw5TzFt`4fV4036qQOGrz{ zx7*$al&GK(jaj45olV>4%M=y8?8SnOBc76@1LFqL#mxNNV!Q}A2s%Ryte_!RZyh>J z&@wqkt;lZp@NB25J8O~MLDGgY#nj%mtYBm5!eRZ-Qk9}N<;e)K0fpB-?mF&#QHcDO zR(tud!Y{C5Bb<;Ye|I9e|Ng*lAqfdLY(|`jU!E$9zlCuf(G3?NUD>#XQI)1ZXKy`& z6cA{zX7YaMbrIS~!W3v|G4_)G**624!V{Jc-Wu;|vQw``!{li}t52j7=!T&Qz8Kfh zh(lb)v3q=fkfoZl^Be?oB`8RtHhR;eH-7`dcElDYU>bp`v9SVVs)Q}UA(J!9de@t~ zzhtUw>Uge%#V^sq=?f1Z7v@Xsp(X0SV_eO-_qOfazLlq$&aTC+DZ99snRhe~4S7S? z*3NeFRd25{G^Ox@ymp)VgL4-f1Jrw!-f{4k+`li^07i27%@Wtpun~$sUprYRfW4Hi zu&8L?^Fy_aDxqE@(b3>;)2P|@hlYku&&&XHG%+zjR_Ke@uWv)|g#%?#==s!!=x9y! z!^p<-qM~*qBGDe9tX__##EuV=Kk`(3$c&3gI)LWU>P3){`xFW5PD#5(Y4X6a@Pgfb z4_XMagR%iehfkjsaq>Y*&~$z3@b-g$?Nc=hN#sitr^ zf9p;{?C&MGbOW?CEHmPE^=A_+${ur3uapi_^Uh+<^!`ZU>g42$8UQ;V(NMB7713WC z@$(Qh*9mnkG=IXszBwDCdnU-bW@Zfi?Nn7;KvkhW!vLK$h;0GCLh(7!i&>;3I?LKQ zh51{ODFRKcpU*;Qd!eGgV%MC{&QI?%*Eagl#S?M^yRE1{1Ioggi%A6Bhb{|No4iH! z0U!m(k?1%-WWF_oW^oEgBwLcB2py$jN^ABmJAM@9mq*VL3R+tH5+&8uK7VWw<@Mo1eXGA*<#)OK(6cc>x&@oQH^Dj zLSc()PYKK)UK?WLZYj5M5-uMCq$?zpFoIj{H?Z@hriHPbb=v7y&Rtt_G%krPtyFIv zGt49lUJTU$I=?vK1xY&!n`t&wXz4KNUmpHC>8=2k#Rue(@lAOu00*9NH`<&VE**o? zA@rUv%h%0qBR$~lj*dCf*H_n+8HQ}olqbc-NkN{1nBM$?0!Mp$XvUYJ6-OQQFNKs| zU^nlliAt_VO2_j5FbLgG1{u4;yMuU0M_@9yBl9Ko;g^lIZB_mRD8+tJOYrYRX-KrjCG-9eaKr4G z_?SL3KMzXA{9{2Hc4)*6=GhAQE5+mbiB+2z-NiA=+h@w$tULts6W}g*#$R{vx!++y zSU_lC0inzv)h-+`H{S~FN+W7kE6n5j&_z?ga=q}&UVeTXs8w)U&9I@f#6k81)hc$) zezT|1AEvZb5IM8FxR`gzCyP_B6bz+vAJ4?7(Yv z#aflylKBWU06+%cs+szTk6_xo_U-G}cUblR9;l5~2pTz9xF0G9Dk&z93mjxv7=#~3ARgwA8wtTbk7Ex-(s{^7aR%($9Lmql{f)Q| z3Co7$>QM&Nfm z{JMT_4DdVe5nmt}nd#})4i0iazb7?42tAtqJ~^_a*AvTEvy5bE_0G`IsY?Y&M@J)V zFrVYJSD*gQz$?d;sLu*a5FCOHQH2^)AtbCk$HzT(42_26ko?mDI2640Az+QK@5_9_ zpR_@Ok{5GsDqzu?97_aaa%r@NfrA6CUoI9FbOFhUiR9^PY+!GCaGmaE zh@%%KUUb!2(6!X<8Pfc}Qt~Xr$iFu|_j=5WiJ9*_sY%xM(t2fwlT)OSjNZJU!^Hmr z+eTKz=-z1L;^K-urn40&Cx=OafbPLYNL-&6Aq)g1Y}mSxF-$2UQ5P|Tf^A{KxB?{KL9aP| z%Rl)$AWFb_Bw_MIqt)@nEgc(v#IQWqeI8iI_vY!*oLkzK39L^6jO6FCiVr|va9!P5bNHaRrX{DuN@QLAQ+bV3*F_A? z-SX-n;mFCQ&bZuwY?~hi@2fPB(216~%j8@mq!jo{dG7o9|KyhuRhP6(8Mj*$zxQcf zIpkz$CCp82@yE-U2x0BPz#MX5c zrl6~|NjO{-aGU+GQ&(4K*c4+&XN-QJHgz9d}7W!AwWjD@Nw z1pu*U<>mrZFq$w&S%B<_hYxo%G2LC4qC^x0?y-QSU$kfHC)(deUb^MlM*+Wb0MU+` zr`mJ5qiSoKI9inj^_S&847mID93OED=*5%{!iWN3ZICUrR}&Ib^`1i*%(=i62)A+qWaZVnbFanQ@*d+YTW5>w^oqyu6WrcV~@rZ8& zEN>k%(Q;=A>53J#;pBU8@XM9%75oV9A;uwy||Uz|Ykj5>sL!yYD+yjGi2vw_iD~W>N zib!<>&|3yJHZ}x=9>}rMgcTO)Ov#5lg@4ClX4lHkpJr9ggRrSa{$@v{MLkX;LjM;i zx&BgAQZf+#ZKrsv+LfO+q@<(>B?ph62fGFC2;5-k4Vsa)fA;KI)DW28TTiDB4HiHj z*s72y=j!TeY&?P@0ZSdBEGUblfrT)>6&GaeK_p<@dI@1(i2JV{0(<@iLS;}3EA~fH zWhpA`_K%Q!AUMxVRN3CYtw3uL6$d> zUVyy(E7*pbs^JJ`ilr2Z8^oSHR-6rUr?%MMntnmc9k{V{92U9zy@WZ9h#;MfYB<#h z_t>-0w?EeIm|L>n2)=mqfSdfjqUQ-F9}Oq`jfsFpe@7kQztPLX03{=&xxjOxqoS_i zd|NUsgTIvEDQFD6fTrkbS2kc=JX%BflS{qfpB2&Z0P3Zt3iyD3CuJQwbEUh1k?AbsW&UA9qL^1#?#tbRM*#`4xC9u zFzE?ta#DHnvIEUeE_Up1&ZO`SM^lj2DBR*BN^&OTlH85p8K(a8hwokSG^ydbS#Lv> zo=bC>dc>^xHOab|?Z!#`X!4XBWxP#AMG3}R4O?3AcS5RO_mY0-tVO)(M?w^t;o#GDKzDXF;0hNHCT6c&Hh)}j&k0=leT2ujZGs;|(cRKVjliK%e}SaQ&2ekg1X)&>k9 zj>W%us5Wms?Vsomi9S|}HCUa5BKgD9VqZ^Y-u{xe2`MRD?CfjMZyx0|T5pxVs3l4LiHH;2mJDT7UaJY=)5FdwpdO2V3vt9N9*b6{FSiGP?rJ zlyFP4&c6G1)+2fxG4sXsPo(m>a{-IAZ=6H6{`RLE9h#D2cghdHwEbT2`lf?Y5wcvv ztk`R!jF^xAw4u!JT{fZ6-Z(V1YOz~-IrLB?(g5bRA1=K&-*x%%gDi~#1WNvY_74*W zav1%w2QfUN#rBLgg=MhkK>Ut)WAZT#NN%#3flg2w@+6XNi)P=+R)ev*U#4A116uP?5ld7Mz$X%15C zs89&@QveBC%;`eWPV;5v7~Tea4j4FUT$FH_8bSE<^ni23;>JfzOpLH03=Oe)J{NK9 z&HpEcTRr6}``T~(7wxpsN|>Gj?Vgvm(+YVm5vY>R62h8ru%l3%;^Hq3+F#h`aVX24 zjB_cEmN78Dt?}CSr<});&R;K*BMU<#0w)ZLq`mh%W?rY7ZF*>Rb-xT>OOvrw77{3( z9BdL-J+*||sEU7I6~BR%wa;$S`j*!u-mHBYn#Pp*l)wfXR?imf*&DvFd_7_HXno$?tJEdV4Q|(r z*9_7Il47-&&1jPgC_UW;91$c9qU}?iS7B7tDF8@cgTNdkH>tO$rx+XAwzuT=IsMjq z_t_8eu$`G|yVl9QWHa(s95d($d{H9h?P7+_ z+TMNzfGO-T52}U4SP0#B`$wsbj*{o)@lVEo?D7ee;&5`n(D{`E$Io%kimPkIFC5tu zx*;I8dfsQadW_!UM`~Y!P-%ys`tvvwtzD2l9?Yw=fT8=v<5lNJ@2#k+p{INGlW znv|IEMrh+CgIRV~yOO=kpB<%L%~4m(7iC~zApAt|7i(lEPM##l9RU*wU0jqkcp3dP zxu10(9X!g#KUR-$lCktG&e(X4Z_{p#mHpu-Iw50|PGTp=UMNuV*swD+t}d)yb@6wa z!LX0ViIVQs6spdXnYEvK3tUjYQVuD?^@iA9RGXN_4gVfjl)>`%|J<2xySnBfSI5RJ zyhAfD&tlNM!EMLwF+`g5nm@cZD$yV!#Ryp~1EE%jYpk;dYg4c9jQT zSUrn-6pnGrlrSTz#fvAM<>%^0S}v23^UUcqJEwsiFA6lnOzo;3?>tse`H+skO4pKK zNf_xT>-;<>4x>tzUImx`yUBAD<1$!%R^t)3L}-?G@qB|4i;+=JaUA$19;qeD7KBp5 z&%I}hX8wqQT=#?6awd=p|Kyva!wBb%7d$3zY~vwY<`$UhnXDBP!goyPx6 z!Sipvstc+h7k-IB1}(b{1??JZnMP zB>8-;6I!dnq_`wAZ(~*B67x0QYd+j8gF|FQ{=I#vTGRr10(^Pt?CC+1GE$GiLL~TK zRG59csG_NJAWcFzA|xW1L{E&Lit)`yDVZ_ese_ju#y5oit^VZaqTn;;`CH*d_ao2W z!QnnO8}`X^Y}54&DMD(gG($EpuVfhN#qBy+()=++$7Ctx1Oo#^%ZO9>P(yzJDjigq zo(NX?b+&*8PwNQcCm!2I1)TkLFSwnw+Ks9}XkdtgjVQ7O^#OoP%eZ!Sup-OG) z3qtPKNJ5i~&^AFw*I)!UXG^~p31=#$ze z;_fZ>J}HV6%sUxOb*|Wu{NNSUKpr$3`0xD6U;b36B5_e06bxz-i+}c*EA6!5SIOQI);r0O(|fb&n<~4q zgFpzRZb+>S4G;IEre|ff#A*Y$7#jNCJA3wogs}|;QJfyd*>mH6j82G#cI$~uzVRrD zxi_S8f!4o!%Ke1)Vp2jy_jVE!X#*4ICO>E1;vrt!WbYv{n+2LTB#%j~^VX~igBz*% zqSKD+x`3o@^j&)V?Afz__lnXr6@W(wR0ZP$!d<2H7i-T@@!^dMI=q!8$s-LrX;&Y% zIXjQfk=0xy6+U}|<2IkrX{UG!FW0pN9@Ml0`xr_Abwyl!zE@wu%{>`LyWm~FnN)WW zhI#^1hXG@5zSjR3Nm+UM(#i@-{DXIHttQ_1ba~u)>*vb?zm9!j-O~7^DF zn-6XdHIx)lteo4rv9>>)iVzYb$D=4do)Pbl zn<}!*Z%_9lYPQ#{Q-AxKKVOQO6RWvGchM@m={%Y`KK_%gi4|=Ot5NLSe{&t!f@xYMF$vvd>uPq#qZ|qFVG5p>}!;FkcEW> ztDx}nPeeq+Vp#k6C!eD%=e?mVBem?La~!*px2?=+KUWbk6v^zk9-Ka0{4k+lK$6yS zFx%{EK&bCI9i19{Rd|d+&{|@eGS4C^OuP^H;m)MU$E>Z?F>5*)*ONK+-uS6RzeX=& zF8{T#Ae8$2#E@L_9ML&$mEHA9nabQ5UdEg;@M5H)jRI~0xHBUIO;gLumk?#~3k#!$ zyqQ{oQ_0I5i+`5Kd^P-A(U?Yjr_^)C-7_j_jiL58`_7^cg>~m0Dz;j(#igY~hYv5j zuR5g_*+7UHuGNT`-n+W?LGW4kQLH;Q>EC94E`Y-uKa>W>W8XoBGtXBvdBet zY+d{kp&jz+^0t9ROhS5!@!{N1dLlxgIwbEnO=u}8ZIlVDG6*DK_IN_5ml@M6lO7jQ zaUE&)P+o6<}1K z-9~03!FoI~0YhVp#0yBc7I7_5y$krkAS!F94v{3guaf(mS zTYm)~@<(KsKgwk^(7g6*S?!W)W606o$)Ianp^>M~HygtfrUp;Q;_|ZJpXF@$_oJh= zv0^MNxMPmNdxT+t_3*_cXu-()wy%#*IGauYQ%vjXa-LPF6!S?)&=PG`jx}4bG(NC6 zCf;!GYAsS_c8rD;6pU#^Mz(}@N(~a%d0pE2Oz*R{^KLjGU}VCVmF{&w%o0$6+`>X} zE2B$#-N(sUPU_s*A@fvSpL5#a3G4X^E&fcJCt8#e0lF6!Gj|8vBfWO*0wZPJt&m9O zFP1wWlROu?<(o--Z&*!Nj@ih=jjVaL-qezMhx{8ShmeitH%8untc~UJug9y#gl^lu zxzjm~*kmZVK>B~h!v01K_6ijkVKf>JQ$az+$<5`|Z`t26h|PR%yd_yYOv`^Z_{HZ- zjZgFxu9Iv%+aZ%`{YIajm0!MV=(iZRT2sqk?TDV>5Uq6LD@>&7n(TR0h7Y7_j$iu{ zuE-875db}y7HDs|r0qLUC1d^(8Zv4kSw0n1HzBj_{8p7lGkt$KukS?b?=+?Om^~M{=EzERv`*HKY#&T-v*M*(d zkSW5-1)sUFbU9swHi@Q(4s-aHSW&nWQCLEe1s%Jx;gjzd2fY7RO{~TBOym_&gir3< z+-6jJF7z&JP_~RkuTa6rD5{Y1dys0@k4d0yM=hwsEvtOaP*eq#l>C4Z2~`~M1G-<& z#4pG`z;Z*w0*>Y&5Gn7@vcmqp(C>fwy!h8rL(0CG7nTdPk9#qfthW`_S}T)QCJEoek%USI7u3ynz!NAXGs0xHkk=S97FK?H@tdk z`fZ=YmYY6_M1RnJygw){Ef8MDz|GYpz&!x`-jQPVowI{O6{gPO0HmV!c@<(E{`M5L zH+Seg!NtSv^QXN$7#=F@FxuZ-bQOUU6y7<<_U;{PzEvG(-YlL|N$$Uge}!uwFU_g$ zTd#`Re?aPs779b+Aa{uR&_Ph;z)^ss1CiX`rz$-K^5{~~%RYK^(aw&5n+2#725Lb9 zl9gqTL({<_8zcA*n4s*NvjtJ~)V?zwGKXL4h=s=11}C^)X43`@0WQ=wPSwno5AHQr zcx*ER1BKjmV zqy6YazcL+E)%@7YDE4Je>|9J$5o=y*wJ6b9d&U!#H=;Xw)#iG)qn^Y>1VvLsP`1tu zx-Wi~lo@df{+pf_`}U19u?kPU+~X3=wGhNyY`hNWKyseQNoDKh0|@h5p1$=j@$F>2 z{Rr9WQ1H>?d|zeO{MzP|%-g7_$43HcF7x1)_Fz&?XVSOxx!L-fKF`hkT;_n9@fN+7 zv-MqvpQTO?4y%x+KntOrs+jP(4`(=k&yOTML6W@gkxYHCeZn)}7x_q(Vus-+KeuPDfcl@y$pcVnKpusJ%n~sD_udK=musetK`da3>UUw3rm z_ngDk-CjDyCZd(iw8ziEAF1p&pu*49=C{MaEPh2R0Z z;}y>$P6kiUp9H7`kvwU<)u&yB6#dG!75ue`#?H~VJnKIhz_a@kbyDw*DgQMWl0~1< zH+{D-&8%P3Sdoh5$fMv&_n$`Xn3NZO^CmLID~4@A(jjT?H$c7CRyCwR&8QOR>3>CRxOx?B zoWYBXVO2-bX+!!t--;Y!y(qq_h&FyzC7r|__oauH{KRzfnuqUQ{6M5*vFmKphUuAN zTZdV5L;n^*n)jjV%RRmKVw+b$2q|v?f+6&DhZopRok^^8Ozy2oC%OAj@aVAYE z^^eHWPY7l^lr((-j1OwH3#%924Gs+8p^J!mICBpkVpzFmiqy7|ku~6Ha7{8(WoKjr zZ%JMUzXGu$%%6x&z}(x6%*?lqyj&2Xc^%u{_<43Js*DDAX#dr9_ws>-#%(6%(Ecgw zoIQDy-nn@n;S$|%)wlt+dz7TZeim)F6*i;9XQ2(2svp?8vzl@Hj!ok7(6;ziDVj!bzK9d1K)b;SK~ zpb;l1x|Tmj!9qwx5uCbXaKS76(ijOy*T@$#V*kSV)kgaTA4iAhPGQ6R>Z zyyouRzt3^!R>o7l=ACnI()agF(1%eSVnO@mn5gQ_F~H!P7C^fCC7Yj=Nbrz`yYsmZ zq7+M>CRMMVdK;MT*ij~ZLuW5oJ2qb4g_#*>q~v~O0z@Xnn^I7~>;JIMuR`p61XpH& z`v#4F#15-V5fhCE)D4BdeWGH!Zu458to#O@4t?Mz_g2iMei|b?8`VH5BbVh{NSuL) z7vB-}a(6N1tj|0z&IeQA|N9iN1eaInT#xXbu~5A z^e_t^hQscI{$gWtEqfHm$yx8bXrGP4u2o)yo*~*QV_z zZXammzY^59NSzcHCk+A>hm`}@)EUGUwT4oDtoL*-4Z{lXP|enZb+lax_fn(Jf+f-d zfXL9&#w;&i#`kkGgPC;m@?AHUgu2yhuWLWKQd@kkZyA$m380F&Rl%bcp@QEBc5KL6 zMoECXJO$%y-^u7|H~e|&j;bc3zF_dP(KFeqmO^jEOmj@#3`O-1Zhq=432nTeP;?@5 zs3g)Wuo2kBBBXb25+0( zzNy3CMs{qSxA|eqZu6mWa3ro&$MVBtj)J)f6KBu4G9qgimo7a*FNvz2iGkt1qSqWY z=@@((xQgSLrF=ADvUsMf_{hZFS@q&$kBsXI_qj2()Tx@N`a1<5OfSBDj0n{ zB=io5c7b6l>m`D!`gtKukQugtPJ%GsV%<*q{%%fADk7}6CSWCR1*Hc^!_Q)!}Y20dbAR1MT0 z9;Q&C8z{bAeBnO3B0FC_3ospjE-LTG)fqd8Zk395J<)sVQW&$fCRDqg3nJ>K~i%oUe49(3Ou*6Uslr4PiCa52AR^#Ykyw5%t)FG(Dyi?O- zwzSnRf?UVAJ%d4Fb33(KNZd|F+)p#%<$HtCZYus2Gbo-=Opc&$zz_{UftUvjn+gF+ zLP*HZ&G9_1m4i379O;)h{m-`S{o!)@?OW>)el_l_GZN&dT%2-mCNgjoOLcyjIRjN6 zN*=!%)^cc>z!ig%M@S^jK^PHELtvIkcPiHYqSpArLK9jd=uh!=5n^kBNY&SF!nH6F-*adE=Xc8Ea0kHOJINz^UpfLwmRQzZbxqdDb}A|$Jz z;0x&;wi{MF-1D|)ik>{V438MW{G)We4#AFCYIsK2_b6ltU0tH`(u+M{7c={vWI69^ zV@2ST)9-{7FHpJo_qv-RP*v*OMP$|k2Mx?gh88tGKAwn3zhE}}#7I3Yqb+fIZd^0> z$45EkopRb?5@NJU`uZ{V5{aHEjZ3lJH1P^edDNE{eJs-gAbk)sDQw( zS=3$@dU;F*m*N7*`|9f7Fe3l~nU~?BalA2cS^;U|;4`!orP+JqD{j}WY@X-)MfFq3!CpxjHZ8`DCK=dc>_c2JVw3YoAjq8}rJ} z`P_edsHq`5GV#08^6UXAj=q_%0xO5nnczr=0N@KNpNL3}^I!p3y2!|_i&2L{1HvtT z%Z?AQ7dNms*akxM% zE-s#UyAUTJ76%F;YP)IRNknDW|9U5$UW7@z*F5|)!2qO{jU!Vk^8c~*=7CtQZ5Q{g zRH%dsi41AlQHTbqkSH274>G4T2$iAM&BpI5NOsO;>4OA3LC?%ydXi}&M-|wpT z{l52)&mVj5=Xv&%`@XL8Jcf0wwT|=+C<8e)0$9PTU9Tc9VnbHmpX2`iV%vN(;b&>s z^p=^on@!E*eZHRn>KCdVm*qf0w*9xLlz(~o!{hggR`tF0OLklC$t-uAUzQ;e^2`>Z z9342n&)%l+VnZdgOa8A-jg3Lmd$dJs1{kOHFU#!vwq?WaF;!~y9U&`fABX%n`)k<= z&FI!t`IC8{?31&y-_|_cdUKt{(cs|VhM@@y=FJoNT{rq-72LjoKbi9~4i5%~hO1VM zeXis|ID@1_S>fNW$vc_2sdvO^c`K>Jqd_U_n<-w=j z?{_wex>7gLKr|1>4E#rB_bhE5Fe{zjQ%k{7LA$c;x4tkA(ZFOx?kOd8J|n}?UD~SS?t#e2d^f99Bu$YOj)4IIi_8~QU&EhXgc)m(g25*d zmW7?Wh4Fxx$ob;PQ7vi{{7kH^Bid{?T)Un&I7vDg^Av1VA|lu2*r9u1IG`XHgR!r8auY&Fqkv{>U?9YrTt+;h@uEbB3Jfn9cM5p(;Ow!$*dVP^N{|> z|46`Zh6ZNzx&e%EL^YRVI&F&>P$B@3eH9eO=;`eV56|^(aPsubh>Vmka1VplUr*-Q~!{H`!_l)O_y>z}Mcaf6z*~Dklcg%fRv{p*zfmr^my@p#N z-v$K-PpdkyS7u}%sXLD!ujf&K^gee2_2J%j>$UbrlZu4F9AGSb25Jj-?C9ihEqcP} z(UI>jP+Ik*Rq^)rhRRib?_O}Nyg5YRexwv^hXhlW!(xf$CWCi2xhgt#pA~oG_o#?{ z4-NDtc8wgrGefdylmGU%Dh-8E2A7Mo-Q8-3tdykvAa-L_Nvk)_(o(wLcl%jgB_)F? zy}0;V)W+)S2^6PAFJF^YRYi2xQ406u!sFtEo?7rV_4n5rzCLckmi*%r6he?mi;PW8 zJGH8Fy5c(vAmWiDN-8R`P{qJ(RaMhYoyzx&8$tBO`bYKv-U$hNlTUS>n4hyfzl)UM_ZW2D!7GBy^8VDmlDlVqUYP@U`h_4VtU z4Qnk7Jv|#}6zR#2#;K0kWua0?Oa5$p7id5aarmd-p$cSUkmrAg?yoy&n((PAOOVTe z2Yiavot>PE$)wMlUNtpj7AMn7HZs@1e$v&v|E)3!}Y6;wMwNMB#Z zMOBHzfBprY4F|V(@80HzD+&60d-i_6#pmt2%+$%tjaWvZ+%m`hDxCo0E8R|pYI|*> zF68hZnS6q~V$Ml%iP#1n#M!e!_qzR^OWAh$T74t>K#iWlr(JpH?|8KLxNX)j^k%0U zYL`x?tym{zUs1O6gK^YXwhg@dIqGmtK(Ek#vqs*;RQ7SqAYDH4p_hs3w#mjC9CfM&M9pv_c-p{!qFbbMgg1ai7?$u# zii%NDQR!c=O>rNI6HPxAl?O(t$B&PKWJXm50uht*L{n5&QE~0c6}WIFv?;f~oUmBx zy9g9uK-P;=&rj(gT&lve=lm{5#ZBn8f8ylFFRw@+mRqq>)@Ii6u(l(gv+g~tuFe^( z9i}_;cqt>kt6=1j{rdFjQ#q#IEGZ1b8DWd2R%@Z|T#9|>aFldGbRR-wa9O#sU;qBs zC>keCn$&!p<#tG6gkBGle9(u?-|t@6?#^h|*{o%BwVT32)3wqScYnISYO|r9`8^`O z!qEMfO6V(>&7-c}UR8MOll;gInclto8+m)Ux(*FCLj~qM?ksyp&Ybz(RKJkJ5t+YW zncip>FrYxCU#%rs&r`P!M9zNR9j-=O*E7i_l9 z!(%Ln!j@uj4%uI;`y&2lLZ}BV0cBC+Y4)-8#2M<_HxH0d3S@2_Gl4yOq#)j*{}EF0 zj}1rm#2cPVOw{;c=*T1RP5rJH)cP&+ow22y_0R`#meZ%dcOy_LuWf8>EUdYtb;*l8EO2V} zPMb65QAI^Xb+zDU-01dl@pU$_Gk_56sLn5&c5Tg__HM7W@v34*2aDJg&!M*H=wqB9aMU$&BgK z$Cf{(6-cA%?4dgG=t&nb(bTC^jY#cap~O(MWnH#yNhPdMd&JhPJn>7sl;br*^F#pMl$<_m(aF6)${RFeYG$ zFmT(qZ+783U48xA53W&w;wRuA5>kkW?n;+X<#CVD4qJprzSyx>=}fA0r?s~RO&VHr zW#%EX9qN%J=nG4(I{2)R_|$W?>97Sss-qX@4K9?oyJP^S2%H3O{8FKXx>8tv`;;zW z<_8WPVxcqp1ebkq=q{S~-^CLr5RDwUVp%9GOS832IaPjsMu~(;$>vIWVIZcL&4WtH z%k#^{`TA4(D40xdEI!uRKG)bPr}@DCP6K=EjPBw*x8%_-`;HH=a#06z$e6HZt_!w=gYdxxmnY*!e|3K~*9mK;PQ8oc62k?b-H7Ra z^?L5tw*u1A)x9@&#D&;7C#tpCgse)_JMZwP<5}`4`9G;!>>;X3JvzW&p;-N;ka+H&cQq3sL; z75vIQAt8iAMpiA&pOL~fLA<~@j)=mg-&m=AX-O9ouS#l2%A4OLD6Lxl?ccwD(;+Hg zviRr`YRY{(cXk&0y6zHMrc!W z!<4C$5ao@(vu$|NsKhyE=$fBh_H>>$O)^Wc$mjg`QZYPu<2rbydRdFB{xg=vk$7wu z-@BqzfeR%0e#X(U`)Y2tyFPh>c{7}uM{;(Kuaq~2yPiF~QYNzNzQr=Fou$tDnRoWl z)tM6@cGlr(Z!N>MagUlCd6`|h%crk73T=NIft0J*e1DH3iY?TZWK#6Vi}Hbcc)fPH z+p)$F)|v3CI{af6aADQUe*d}K{?vnOzHG%M*Rx-wCMje$kLq$UGG`X5bipg1K@=ta z*siFfkZawcJao2K4_Gq7(2x~wf*|^NCm4Q6xxVA0o9=$hkMEf<^CNv1lM|n&kA1U5 z_=yN))XE9Zubnz;dD*9Ls;&6i8G1Iyr9+Oex2_hVD53R9Ny&SF5=?iutS}3Z&X~0j zT_E5TB~8cgcdGsS$28B}?fUn%NH6Zk=gc=Vc^ED~<7CpG^`*%f!;&s+e|&@T1;Z4U zn{%N_A)<(V`vg!TGcz+I&cA)Mk62Fmock;}anA6!yR9t%f}8pdU$AV&iuK*C z=gu9E#Q|}|f12>CgIr;4u&EF{S~-0GRLwDCvbZj~?y~}dtnH*b>~{2D7=QkCt_d8% zn_dSEOd4}{e*f`<6_Cbpaun|p~)WJ>!oFd5=GnY9Dms7)z41}*P^*{9eg6$za z!ps2>ma%||I-}^pgQ`KdCLIhA*YfZ`GwRi|DZJLoXUjFrLZxT#h6#aH&4^*cWaZ`8 zI;QDk;K*EmfEd;%9E7*tz`d)8Psn;3RlA6bqo&!qZ5!~g&3;OEuMPT2Tid?d7mHQf zgnT#G@ZVL(7swy1w)@(Rv z*2j`^m=?2hr%p-p&H`n2S9o{$&>_#Nn9Dgi$IOpsyfY@@EXUt^{(L5Xj!0SU?N!OS zN=mDooYoO=C#A2}HcbjSKIfIychX2ksr7sJmTcn) zeOKEU(hjkjn@}+0m2iV5MO?Y^o56ZH@}mzQ?p4d|r@=r*P9?y3b7;?LrdVZG&r0+E zh7AL6D>_}`st+8C<`)3v0n6)6O?L#HE*&&&+Rj3~sEOCIe;E5}>}a`#;Y>T#92$GA z+D5}Qd}F##mOnx3XM9dZe^-WxP`-fi9#W-)kYWHX`De6ZTEC!68*mO}6o zO!&cIo>!agn|gx&M%BnWot!29ZkO=g_YMYJX3j-#ryMRNDe2?weUASFyafmtq2o9B zF!in?a>BV;yy~Jvg7n__rgR2hR5ecaqf9mSp}X4B^wGt^L9n#AbZzxlN+7y;oD7$b zq<;a{<;wuY322&YsG6qc2KK(<_mE#ufV?pmz;(^>=R#X-pl|&8blk;?%Zf>7*R;>u zx-~wfHS=Y%<0P2^_nK1G-IgbrO0)GM=W^8QR%t3+P;#DDrys&$v@URQb5l`JAU5hD zd2Bw8?GI;gys*{S+}y>*`DHSvh@p%ye&wa!(9V_lXeP0wR}60C3JL}734wM3sszF> zr1AbKWCVr6F(*yyqcC@Nx=TDYC+?&61q(=QJou;2e!|7!is;_YdY z88@=mzx~?qV`pkRM{!ra+JEK3d$@P|_HBs96t|A#?UvwdWiMH?y9xE&0E?OdtE%b#c|9YKhgXCf6iqqDf$OZ6)wZ+d3JU=4E8C6 z(eDtJG5-nfJ;a09$LH}J1>wFsI}k$@1Ow5Fb=N7x!K_yl=Px)d;U&%eUB7;P?$+Or zQf$UBdE9GXM!|(X48v&Y837#X-)1S6P{{=_ejsoE+9Vzgeg3zx`!gn_a8ojtFbvlfTvKFK!?Q{=L}t|tq4?OdM@~+T2a%Vd;>I>Tp0?d}@6wO08wAnXmMu*EQdm@Ou~sCk zdg9eUv!Eq3wzC@`V&Vf7|CQYa{61bt&gUie3l~-}nl|{ll)$vyKI^H%o%+{a4Oh+V zv(9&V?Xs`WJVv4Eq(=N7yOw?+Jfy$D4X?wP0*Z22>8mg%8B)6*^I-Z!3B< ze|WqClm=W6W5A+326tLsmD0bqj<4~uSvMu%$Z5Cwbc#{_dnmsoa^4m~Kxodd-gUf9 zee;>eCG);MN$&gDEU6yi zZ6$hZOG~LN(X?sz2$@71#hQ+ql~8&0zKw_xPMP;QBzfYNkK#M zj~aM9F%dC9$)yh(0ZTdu*LHeRz78%Zdl%;~Sil<6dGqE;fbE5ZWUkpVf8oM~vuBG5 zz5_(M{u8%)ueO~qaNuMss}M5{aPG57NgmxvvoI$6h>XzJLsZog5}G$?0G7Up8Iv!{ zIsb1fTKYyJ;o*r3;!cUahW)c_AUb5|# zeCpI_xJERvoM&Jt&{L$;^xBa+pp~3qN>Q%qh1XALVi--oWBK`U8mR2{T>UjrvOigL z>!B!s8i@WEOz)vy=17-#yP)9R+qbjN%%e6SF%!izw{9uBzb9n-Xk~y&_cJB?zviEC zxpDUmcXEdIxonLxZ+)c)6&0H(j~Y`Ol;OpBLladVxBu>KAug8%8mx0%%7Rc5Hug+( z7AL^e+Z#^IP^hFTpIue%NbSll#~CN*V1~i|J4l6B;WR`(?5rtAr#h9?!w|gh*RMn^ ze1qNvFhxwUDHuP;);5-B31xob#O+YwwJbo+#YNPgZYj4Har@>kthiA6CTMEocKu5G zQwI`DU1|D%!s|v~w5@IAczd`S#qgy77L1<7=r1;Dr)*PAtl6@JZt?uONio?cr+=A+&h+f@guUfvpW_Yy>iW(bIHktk5n&( zt$CS~os(0I6Y%22(VH}>b_vMawr&+r-#J3Hy6w*|T~T_)3RzRzeBdGl?In#ZO{@1> z&^PoNVB`%JOKKuw*`ce8K76n6H|5P>DZQLAlWav1U!~io->B>p+Pd?qdB`c#_r-7* zKD>J6$yJIR=@9jlC6k*suUffMHd;|A3)-i14|6trpuwKIYSn+OrLkYz8gdE8&9i-+KAkzSH3ED+7lc|-Bc8TFA^}|)4AE&5pomX`1YwI}H zd0#~AlSH$=eSAkN19m(apnXJ2U0fvMx7k3*`T2Eet^)LL+TCv`jQ=OU`Hm$m1D8VA zAwnH-zU7Y>*6U1DiHlOO)RA%Ub9-rb5s_qQpZk1^LkAC5hJz6p3>xG@p^V`~;I>R~$3-|r`C6)r2E|b`KrK`*G zKNaiMep-ZlpS$4qQ;+z#VM7}}t`C!0wI2&|wxr<7>(RZYwsyUj*NY72-SDLL1q%(A zgqR;CP*zqJe-;*ZfS*<4RRXM&)syJ!=KY83OD#bk+6&P@Irm9MdOF*tJ-UyC8gZJ+ z7iSC_i%4fb=+xXMwiMoZuI>n8$R#6xW24bKd&s%;^l$82VtEE&gH4p2X;SB_FlG)g zxkV#@k)SuH?<>y!5 zk7dI^tp5eM64j~Gr-!=Wb>r9IjZY_C6(k*D9E`{!+-Gd_0e}qS+fN+1&%*j!z*R&T zVBXXjgxm|)rIXXrid(Yuq>iBH;)UiXf({tu64kUO0cwoY$W| z6*WX)vOu|%l;lcxymhP5AxjrYf=ea!#OTvcJHC#S=`v;Mm_;2ErvZ8u6b?f_SSvuVcx-I&^i)3;CKz4^w0nGtwX1#odd}Rr6!23zW}BHUgB zSVd|{N=w&`o&`EZ4Ge%BpA_b~;lr$82L$Ay zbPhD_r{mzqCT-o{=?aeh^bHJrUU-Jee3WCY$HADGLcO7Emv>Vv z1fJZ~MH)Z9KF|18I-{?N|8l!yW;>2$6TEfwx-5RW{NYZSRYyT&8UMHZ{_XikDQYdl zWq}S|S<1Jedej~|d^imOfSZmv=bv-a*4D;lRouzc9`H6BClCzM&YvH(ADt`6_e3tu=lN#Vzy@-zSGzdE~2ukoXB z`3I{FdQtRA)^r9RLvZq6{y?ej+Mh*id`G81y9Y%|ctbML(^+?ZJ}qtJ4+pj;`}>>l zo9`{t6H(LXNgs-g{2rBpxP&CY!C+?yb62;6qfC0drLL-_wJf?l+9ark-U6{8oydk2!?`x;{#I4#l zRxdu|muO$zXf$F($2OJ7qHfO&8Jn878JnIz6er1nWBz*e^5yGWfA6>Pi-;(}9tQ-l zxYoR%5C9S`@tT!rDPTME-wvT@rNy@rVW(<#Y~QxGeL%%-tumi<=3^`SXH( z;>?-SB624`;4l1$jPX|IjdEN=f`e&adTf^`3Z4PP&kP0IUiT(Q_M5J>=??YA=ylTd z$4+!JIQUiJklxGUKUc4x-)r7mbKjyB?`pfLWYb*q3^SR2{mBmtiM9pIOwdqpN^gHZ zv+F5#%h}hjv!Fhh5`)hl6pg?4?%f3huO*%{TiOKswJIW9X6C5TITlH^YDv<=g=wog zfqQ|S9WKoO(H`M23Gp)~hKt_|1UT9>0ZfeA?JpvD3efVlp3_5%o%c3I6g<<7Ovnm~ zI3I7k;Dv9O*se<2nV0P5E|L*3HQmtq=hM$;6%uA`Z|(@ z{rewrEeHtIbF7tuKeg1_8VSeVDWG#URv2LR)3vnf;x%kO6XM@P-7IDdJPBw9?e^B^K6=rgux(#m zF=Df?=HKtIv7n$}6FncCj#QhhR1VF)6;CTGSuZwo$`lvOU?}+E^(xTNPs@7GPZ~b726#r<{2w5CPS3Vqzx|<6wP;T->|&?_EVQ z3JRM%JV>oNy1&%|+O>N2lxZ7ay5p6L#KAuG@7`uAho4J&y*V{VLQ!%4Pu)Mw+s75| zgL`>lW1>XchY?FJ*Pw+g%S@UlY%%c>pK56-nDfxfR;uve5+6qfEfiu^V`*$aw}n}Qjk zhdD2r72x&z) zW$I;D36KbxY_>hi#YUb#xUtK?znMivIoNCtQ;6BKSr64&L|<%vZ0MjtUQ|fr2zZYy zB;DgYoe<62hK4Cf30_*LsGDh0mgyr#J3fq9Fl?A^*H)4eXOH2kT(BnOGc`5i>3R|g zBT`ePtF!$}93*_j`o8^C&@^RnefpH12d~)YbZactXfo(JyuQZ_w>{^SYgLMw-0%w? zth+jf3ZZhCvTgygOY z#~Yd1*?Z#sE?zM6xHodpvu;yddYfJQz2<33e7v#av*m67*vu!!%yS%#<8HZ?in=%j z9f05Re!y4ukkA7IyF^b%1K3Sel3Ri{unL59?wmQ7AE08mOy3Rt(CMX*@afxDPSt!w zn}b87;1|yHX7#DA5W2J z>)QHy0rPxw*~k(4`Wvv{0>yZ%Ta0}cseGE|ochyjs-&fTO8an~XUr|4$r?U%s6#JC zFkCtakk2`pJnU~X`k1;26aXrUD_4AAv2w6Uy7)WlR#4s=9O^66-pO>Q`sK`n5OK_8K^S~Pm=svxO2=|$R2GQ z2LK}f`6EoQ)NlVInF-Vjie$hj>ofD3qd=;7=cQGBaI@-tY@n&{|1>!qXeKw5-C83? zls|fu=>CW8hr-XHsDp+?$DXp6QmSUJDEnorHGBQR!zRDFb8dgSCmgGPa93GE%Kxca z`h{KUw@juV{T)P2?ch)Yg2=a28Xp}W&x%&I6ZDod{w&quuu(&6hNQ?ZXSH~s={RW3 zoYTU>acizsMEO`-?4+%h+iGyF$JSwg{~lVtZ5VH6R;(do)~$R?vc~#QpSBh+icDHe zmdnvwKH0O-LZTE(8t}ic;`YGyIcvO|3pOcqH5bVajNbK%O6 zg&K)Et`dtD--=gPd!D2%#^jeLpbJ3|0s?MI;k7di0oGA!6~kEiW-l8M)JIvFcGq#Q z?|&Z#V=@WqN{-i{y(O=FEJLM5M+->>#`<~EQJK_|*DT1j`URz<|@i}K`MhN^wieI#x&=rs9 zGKLxixBjxq|16-VgQfzX>VQI!pkN!ok>;V3AxJaOe#YffC_iE}@Z!ZbKmx#6@fDSh z@ET@C>G;>GQ?m5!dx#-aam<+F+qVO1Zd;!g)LVZu(Y~nF9FgchM&1oQsBm1b>s+7xss)9^7jLU5s{jS*C=@1 zai+R_Ich>Y=4E6)hF#)@DFWIN*oa$di;yjhLcEywy8yO(F#U7zbcN zj$<@6@4rKMR^+I3Gkrg6Uu$R6IFqS5TSIpa{+v8;SeMD#d5@G@V#G$wosyI+BPZA5 zR`magj1});l@^Rd*7a?X5EcTijsOw_j>e&k*1wsEubgGn)=;|}w2kM8ziRZ$;I>yN ziPpNfO!1w5yp&+dG9ti{793fn0_B+1I5~aC1$E6QzbM%yyNR)a9*4c<$UoMuP34V3 zK>&XR+`uwom$V9kDywA_6B{F9yCmdsAmS#CPWkhncIU|{H|{GgzjZg9+VNaI zVJ;%8HgY6Wp>{y^^m8{suOzKBvwKhYduUf(=i$bU-?Wz6loIs$=E;G(b~UqBo{ml6 zKe^|K882M8>28F&)=sb)icBgqiC(>m@812zXpFn7rtv80fmKPI-{mFd{0Wqxxw*be zmo6pRO`g0Humgb!pBthq#|d=-xFHwLY+cA@b^QQhNCKx% zzhf%--ljT}%ZVyRSZ4w%s_*tp|MjR(eHcMB!2ad6@J%KS8@BQvaQIf%-q!ZT2~+Jb zbHov=iyka{kRvX#9@}u| zTcYHYMZeeT_$VFy=6}s3ebvCmF}F0dOl^Do4d^G19UBm?p(V7_n8|Y3^F!}JHl@n1 z9P@$tHii{tHp|2QCeB0T9Bp$lZoDD4bn7feF<&ZaAQQ#j~ll(lT+{VR$qJ zOx=+hdqXl2eQRop)p6WGlSM?srkmod-#)rETz%*-P=}!*!Rl=0v}v!}toFsmX0!Ph z+_H4Nj)6f_LjzcRtvuFxIDafBSb~l71KW?LA;;&(t_hEB-Et=8)7FSs&;xtrEQ7BR zBN)W2sUCK5l#)8oDRe$u)Yr4|O7Pjt&!35ek%vG`pbLd7&eP`=QFIU#Z=Wdh)Uv&L zK6|cl(#4afO15UN%pCk!D>SD0GNDAkubnH|@eiElOoC$3IPdul#A9a zu^O%OK;wXp&_8E3Wiz=#I6A-5zj==4~6bqmQwDeRJGt$FigVpZe)0i(c8 zL}Z4QQ>O++MJcu}X5%=kiXD9gwLRT{@?&=HN?E1D5o1a1Ju(J`yq?AiG#{;AEA7Rb z24VZnkHM%>F}$P1L??!7KV}mV@y8?q;|Kz#;JJ05jA`tR!*AjqbbFL*@Y!sNOS-aT zWO>Qyku~y1CA?NLE=TbGy}9|6_0W{@KX}QggG;XdtKi!*h)PPNIdB}es!N_rPYYT- zYD&Si52zBn{Pv2ZT|ilO;=92m$|wAhyC@V9y3bBCMf7%igtz;y0$od`f{0iK8#;IPe+%G;0!9h?&+h zJsEUftHR(5 ze1;q3aV}_+8$*1r&B1Z5)dedb=a2DesxQmXPdk2LmgI#=?&?OSzNa{CoQno;n?w%! z$&*h)e~zNw0#g3m_UB)!iU*URtNOlu)aX@ZNid?CA0Ctlv+soh4~B%81D7Xs9{_C0 zEL9YQZf@G^zd-?dzZ=d;q2jr;GzhS#!&_Tg&KCimY;8&4U)67w3vEgl(fKT|`$hGg zuMR?A7YNJxB8y|XiS(kEVlTB-zN2GIIs1pUqW$`j0HGxRS{rz|UOw-2`t<1{zwq#{ zurP{Fqj_g0h%D$joQST@M{@?qi}-9L)0MH}CT)%+5`L)q#%N#y_$1C8@9#i4_|s zPR)vI2jJZ+hQ$jXXC}CLSQr{r-K_1)l!=@I@Opjt^}mHkk`FOg(Brdi)7&J&*1>j>@P|gQ94Z) zokK>EFBhKLrDYfiXJ<}MyHLVeFz!%vd5rS%QQ`d!G`Nj3&oO*J<5l|@Yty7FSH}Ga zVl6HljQ26SVi~jO4nTlnKaby8%?A6B-|Pc8Dhv%7i+01C!2@PrY1iLwNzEds%M}^M6 z`rxv7=WW}DcPB)Mptg@PFmM@n+g0`Gv11RnCnhDmr(}OxKu<>DC8lO%bp6DMfVV5x zty#U=$tm&NxgMg>)A!aOfBm#mMV=GL#2#fN#^-!8d`)H~SW}gd<$n+l&^#+DD)#Nm zy>)YSojRQw?Nq`X3SY3RJEQIvw{9~UHA6f7d{?Xf@6S9PeUIum6@25$)vJSt3|Zfu zbrDs(Lh6&n@;8n|M>A+)3O(4s;Lx_+i$%23w30WgT>oBXiGgZr>F6NoU)j>|9SBg_ znQTB)G8VZX5%uTCCm}UG#vj2^M1+{Ee_H_~mj4b(;RvP8LY4{CFvr@}__NyuV@{FU zfB)T2@j1*D{*hR*eCZAoxG)orRc{2Ti!62X^Z(gAlfKB&-NdSbz)rv(qpyz2zUn$3_IAM&A%ZEe%9-=GrR}`;_ITCeja|ET zZR{4hfB3+Dn-@pMa9Jn}>#nXpa5dUXoNI+FZ?ZG^FQW{h3b=p21#LcKbok_S!pDwP z6LRRJqsDBSEOD{FRF}k;lU;QJ0*yUqKYsY+2|W=fqwmGP)o7B1}U2TZ>0 z#HCA-o(F4R&&96q$Pv;5`G1OPg7r|*jU4$yG0T@GhJ8!Q9VHFzwM13tQC-w15jN$R zh;uWjhCtl1kO|H72$1#(JJ_m>+y28qqOxA$kZwMz;ur-Y=G6p;8@jKYOZo1iu} ze=CrLFu}_Y!}NX;7@Fk6^ldLLd+uy^!0&-~5SVdzge_VU*CI_ibhwL@zWPcjx~nI} zcm|J_Q&lbgY>h&!&4lvdLodQ1Yd{!LzK_}O%y}}6;MI1VGF(V_Pm!5pS zxrN1;^HZOZf4Ep^xL7f5P-nYg)QD1Z?=r`JyA_{eevh1IB1wg>kfQMMQkuEfVOKAr zIK3h2Qfjlt_B;vo#3xT~7Ft(k$mZ`2Fet2PXv>#(ythnebl}_7uO9aPHnH1Wm2_}? zVam^8q-np>7ieTYOYNFaN@?BvhsI8zeMW}J(zGF2Zu4k|;47NGj1R7P9<*@5g3%-n zfk?gR;wmA-Y+Gt?AD6S~DIug^)|*kQU0qQ+^5gvcw7a|eEMJd?*HfCk|2xF!OrN!U zFoj3A%DJ7O5n{>uHX|N|mR1iD(d#sF3{~Yx+l@rDBxI$%namFB-o7pUIibExBIOn} zYPQdhe3XndAs|X%5mf|U*y!YL=JdfuCu(7$C8wsY{HkR=oPXqJ^;a#Wgi~Ej1-4~XD{X-9)CV@_K@!l{vV7NT@m}zHSb=gj*H>lP42Zzt7e?`F)007V>Qi>>&yDU zI$`(U{rj&KZxk2jVu?n?pMg{UAcdO?GYfAVK0iF~|NQ;^hLMcYOF*lbYTdouGwy{z zAf;hj`RD``2#xgvAMuL2;w3d;fV605w_ePXd;Tb;Cl?u0cQBx9y}}@CHvG;73zpBj zCl~^3&Y<<#nQeZ>w_`h_5wayO+<*sIke8=EaNtg#0Mi7Ti)=>y+YjnagyoC?Rusje zr{k-|Cc`J=M4};shW?^_T$aJ>7CF2G2x4f{1*Sg4TaX|IO8mnzJ%JZV-iXt6-bFG$ zUGoyXR2_w z$44c>z%F6&!2Rc!7e08PqVJ0IU^X(GU%!7_Sz6*G*1`mzufh;*N{D=E)+BPm-usr` zDqV>|4N^xoJQm9pHL6O0THltsx?#P1fc1}nUAwMcy}G^qPqYnc>moFDxE4$uoHeWW z-yx%VnVoRp_+Zpwl&;~mo0@s3f*O!7N36n`C%8EMEl}hY6*<5*%qSUyRVEh!Ei?Hd zam=u^yvp`x=aD0KQMSU&92Gu2N=H~2kAIQi%EBaqvN1XwOs9OLZqq5EjVOiokPqh@LTTUL(am&52v=Mw@?ZS8w=B z97KAyafXH;>+2u;hFA|hNGopy3sR97{j)24d5^?+ zwH`VGr066mC+8NaYMRuW5M%@J$*EeYuT&sNjY#}lI=$vef>|$;K_y4WSL7P(EVlr4 z0fE*i9yof`h{gygPkG{2!KtC9hYd`PgS;XJhMZssJ?wBFvvwceH*~iXyuZbVhtHy) zc7BdI5^S}(r=~lO47qacnqZfO{Fltv=oUFua}HW0OeMENMr;mCIpP_7iA9GePMvB7 z-~z08WPzL>sXxc#Y+~Zg0Z@6DV=>3)1tmP#uy`^iOR%}Xs$$zVNvH%@?M^C3W97o? zbzmzxZdSjhD{BuQJ|5i(aUCchw2axqOk+zGVKvg17u}%ZJ*^Iok4zl%#c@q@#l-uT zuUv+LzAY0ks~GdjOkG{4e~-v={1tAu$4~Nmz9DE~?~7fx4zp1>WGP5D3DJ;vFW+I* zxtV1=?%P(`{#v!M->*3v2mRl_+kQCNLqcK^@r7h|xl2$;2&nB!SSeboHLs193S=un zG|s@ohMhTi@{U``sO5wnG8lg)ECDXN@o21J934EU$-i^v%+n}knmw9sqB)=93_ENN zEs2u5FyIoQ z&t!44qB-X)0e5sAF-1cv&~)$t@^)47=y2Zv8bl1> zfpd#8G$u@lbuNNq!!l|1$CZ->q$DJS(INx9_H;S}W+Bjeu4C}Vkc$SKK*}Vh@cw-v zNOJ3_o%^#?({osk$)9-KBevXw`b>{i=r6Mw>YjXF4eI`He7StwMX`e97 zcg+;D_;6Xprp_u8YKFW|KKwBCf&C1#J{)I_F=HwoKOUu_vBO3bJzb!N0G#RmJYSRi z)TPGi>7oDo_YI#9pGRIq$H8rS<5_Ts;Ct=bmIG^VxywN!9(|;r`dS_;z1t*G1}}hk zg~#G(ef>`~T$3jGf;I8s;Y;)WV3k6q&KrA&NCBEIXS=PSnR1Ts^H*W}H(LsENybUa zNkVs^tLIWDr`J3zGBlGfp@huc{-C(Hm#B|aV0-JaxHy5cT67wt>WY$*^uh|k`~Pn_ zE&RoH00$0&G4YgX<++B_QPI&bva@W3U5|g?lJFO^9?W+6PH>zKo?7!^gqoUKFLwol-}Ky08Gp*T5HSWjI*gb5;ca72;g6Pt3Fgh@4VXZZm;Ex zH4Hv4y^T$bfe6{bg-sesC%>Is0|FjNm&}fJd;4N-bC_s2@`Eeiy&d)H|L2MSf!hns zg6|BEAzabL8KJ)789x6{!)+Wy-8WCU;e!If5Y4VhCAO+RzB?Lz=aYH+j=9s&u!<=zld5I z*q?dOj`fq10wq$ypdJFR*0CKKgmumCa|}RWVlT7&Jct5^&YhMO)=A4Z^|cUyP!Lc! zkvBibDz$`xS#I(1G5BzhI0`?#1(Q^s>Yt_fA+|% zy277C&W9NqP@uO6<2GnXJiOljL(^+<7VxaJ-1hI^zKz}fXKX>%8CpOlI<&ov%uw-b zOq?8WBDC!fh--hHfcQ#>CX+;k&4GvZnqXUO~xQfTbH~f59 z?e$wy5AfH@{9r)Tn5DMTG10Vz%Uni(Ue?zj{X*B_t4p4AzVUrHA{xM45P1CwvGoe3 zG;jv!-D1Qke#UEn7EUISnUj=VO8nb5%S}8b-y5~G0mGNl=-G^e^Tz|SO+8E z6BsmMkkxA=vP8Uf>~^4ApqBXGCf&3HttncFD*z=Fll!J(v3|CLkb>ZEtvgbJJToBcTK%py!62D0Kxd5>|Qh6uwO? zkrsI1q)Dxvnm)qzE&-$0&>&q_jnFOA z{L8h$RUSWszzY`|i{sAB`z0fpkT+-KG5ZmPX%UC6+t15a?ubu17oTHd=l3}7nq9W< zXw{Nmwh8!z9C^zK07L;@4L*&71~IjNRadvmO7dRW6=8RC_c_qdI zd=B<>4jeMX!ef_&f#5MIFrP~4k@yL^;#6RgU=4g$dO}0X7OaV4npP<|ZnurxI3VygDY}#It#3ua6(&b5gIK*U$>RIQ+pr|F#o9*B;+J?9d^}H_Il!EgrO7({EIE z*#^trg+u$)&E0=?HJh+j)wgTqo^{Mq0qCNp6kzq^y3!&{nVXnrbAQbS3wlT9O9T5U zDeVdj{LKnhflnS5_*Yo?oTX+|-Et+PrYxEi)@n>+N^AXzjXR@rE^d3ID_WL6}#``Y6Umdzi>|7q5S@R$C`=p z-&eGG8cWDxuqS*}yKKWrlj3Zq^AEHb7GX+L{z->TB@AX!p@9IyKN^-hVvEyg76TKX zXpedym_v(#BB?EdGz9l%%XCTtijvxQP_YmXt+boSkYy_qapDp?%E?!d8<;;~<8@?A z|4jZfeeucDrxCdK>ZfPMvh9Xjz`FWvVog()d)$c=nj$TmOCKvU1b!2p z-UP3YeXLHLKmVQVj1`u|{ivtKQ;r*j#5@=gbG~@;UH3c7yK4!zEBH9_4|URXPU=?oQWSRD239VEGD?orsW z6=yJ2Pg@CO26V13_sd^pX8IIo0uvK4=My|bkQE@dfnR$I(;co4#|r##eGfE@o+vT? z0zSr>kL&wLe6)g50KOA<2N z!xnz_|I5!6MC||v;5`VDFW(7O)V*0)Xk}rs`=1Vq=}%wUBTOy$@PEkNW2;-IoSehp zK}h2SOG?%(?FH?nt{3R4aM{RU){>P6f}9(+QXLcN0h{LS4h)2a`kr!6Kwk6T5NW+O zZakG%$MYR@;2y&SA}vEu;J8Y`44^lKYYwhE{~fxRbW@AzA+Zls5(R$!vzQe4Q_KMF z78iqXO$Xp%J(|3^#=fhIRPedo@3O2U-h^)W*Y8_D_1Rzg=&?hq`R0BbCfmN4eCLb% zy6@Ygm-*KkpIjU3RO+fwJ~^mqeDv*)N_Imc?m3i=T%y(`$3?f7(1|};kKNgFaGoXS zm)-l@b20N#Hu5gX%S)(#v1`ZQc+#%SBohHOo*IAwM%a=TSlVv~M_{SqUw-0Ks>=n} z1!pq4P=;;&`xU=mSqNp>(@i5J|F(`WSR&K(n?wn?#Kvazs#SQJpsnZ7i$r1O>o;sT zxNqOeRde-P-T>h6;F!(EXUVb?j-^~PfoN!6c#6?!Gc9oFO#Xa}TJS53f;rJdu z>c&8y*8~F0y?{OWv_741v0}tc!JwRDD2!_MCd?7U264QA1qAd(+;(vcYpiGt4aL%Zj1tM;FEzV=@GgXox?6opW5=J+IIR1N&h|n1qDCuJX3jm z^V>p&$l**9djHgyHT4apPyvEl^Wy5o>(`IkgtGUE40W~HZQ|d~y|3kOSPV)@+mlH7$S^l!$g1K`u*S)ngf46{#0vVSlXANNUGdMIy{Qmuv zm&qP96{?{X3myo_#9ipGqGC&@ z^8AGXd&?*j8w)h$?%@ow-AjI%9?Ct#!GrT@M1clGgvD2%KK&u9jqv$Nzg0b_SfNv2(I#6uCN_2(%7&I&AXd=0bv@Q^+=!cYHCky=4Rm4X67;t= z&UM8_MJGlsctXuN$x`*)nKQxk?phO0SPw;gyTPj>R$73iIzOskD$!!ZpC#B6u`(w$>-C{AkcT24v{Ij=2@n_Qd>ncCWAS!N)4r=K zW_O2xKI4{e=_?>9K0cADE)OgW<$)8Ck)EV+A8J2vZrZFP z+bu+yRNdbVoHl_>uQ~zdL1*l-de@XO^zQ$!>+`>|_p|BDnO`^%5Tie3YkatQ>lXK| zeBq{L)rtMVj7&8l4_~uOwdPGt_ zbf4erEoTFEp)2Ptw4J-vKB4!qE~a)P3Wt5YpS);@m=fQGR8sHww<|YS({`^c6_aH~#0!~t6H%hYW zW9)DY#T@ugOVe->=%)U8>D3MnY8o1Xu2`77AJqM2uR7CDpqr>hOg%yUWl;B@h#t>z z#R`kBGi3%0fbl&-UA^?~`tjX&NV!q{5jxP=3&w3NP09Qc?7X%_v8GwUCf#|}s{4BD zpdZnzqH;Ih)aCNlZq?ahaou&c{Fgm?$&8c7JREF|t9>7j_So=IE5!WTC87p0P+Z>A zRqy|uS=DY#B)8xZ8~3B9xM|W1>J}#RKvPF;{2+P>Fc1YygT2kag6yghJs--&|6zWT zOO-3E3?_pWQcI;kz&U{e<-g-jC0FYmp}BB~K7`{$<=ySTR80z{%_^g{v_dUUj;_1J z2B7V8=RW2Z$H&LxWa;>-W9>88EkN?3_%mnjRsKqbYC=9~ETG0A7}(dw?LB>Zi1RpS zTU&)skEU6xj7Aa+|4hJ$ii+6Sk;3}vf?2ze4j`S<7(B>H$G5-n;Uh;3Dedqc;TOut z`o9`j(>##D-PhnA7h4)#`K}GtFMc$DAtyc}>xzpfy+KbX}z#uQ!B^8$C@yfK>q)uaG z!47vvNA+y41cbN8)mTo-PL%?j1Ec~Iaq&q>c@e7O!uwXVX*ypKU)fVphXGtK5oTU& z2vd5hxrv&9#c*U{Kz)RC63+XcQr|Imo7&4kJQtx$#{4C7-5=H?#qn+Z&ua>^c@hu} z0?p5dm^+WNK0EbFKI@b{C^z$QMFlS2?(nmJ2vkuTX!LXp=@oY}UFXA47%h)Z0PO#@ z`08Y~MSyLgX)%X#?);ke06O}^_XE&IJ&%Hc*{|UzvC_49s zhBkix{_MPq6;@yklId$6HIalMnnQSAlV`m+E{wtbkwPJYx%1Xi$(ygv%GnTW1G`CpWzzu2e<+?oj4SBP?b71(NE$ zTUe-bpqC>Ib&99T!VbRI7Y-7qmCmELYy+hF^t*Sb?tK|8%q?bBJwI0qqsbfjDsv;c zOYc8D9~KmlnDK{czK1p>O2ss(`~8Ox?J}J1a4@cP;Sys#CTwYkGV;Gw`NuwBK23m?s3u1PdJFR&H`JEDJ3pQSl9?fY^M;CD2yWwVDo?xsI_DKhG=-b{!?|a;s zOm0vN@0Yy5*UHRHQ%wz@hGw1yZ-<}I$^T@tE~#5|zB-hK_RX>FPgBop=Eo`ZNonQN zr&u5T;O=VG66ZM2=;Ypc3c(!~&ojO8Wz*qt&FC_%iZHzI}aaYhtD;zol_AM2| zqmXoZec%$hPe5@CAW#~v33Y^AdQ*msyvN+w=qu7JmTs!@u2ZaA?`ySmDdFTDn8XyWT~g$=sP6p8PB%ZM`(B2UF=+5$fkf~@t9S1M0z+fAWzu|9 zQUEu?ZVzTFbifeu_@YE&A&xX`_ zzjne#8;5hdnG1FjQI}#d$+T6k6HOS}vXJr?*uT#3;g`{ZBy0!*N|JuXS~tn?Rz_^vrnKFM35W?_WHc~GG-6xO}-Mu z76ts9PbSrplLj6@N$mp>hX1YZQV&nWd;-=ni6sfpw>ULEzCkb77K)3Cwynf~b9?`n zJwj~)plk??HkDi^dP$64Z1}O`3v@G$~exM{Mh8kB!>; ztP2jUo&r_95X|pG|2G+s5vn2L&<=r)dnB(8kZI}6)rEXVIdH(UR!-oQV^^^i-($YJ z_+Q5^j?b!N0G4xyGOS?Gd}I7NNMNDcyBIZpaB)aIx$b#uNSF1~`+sgIe12A!%`Dm@ zN_}K6^tgsv0L+^BM%BZH)^+e-8NZ47nIFG*|32-@U{dbTp?8al&J}(=F&I??m<6CW zjjC@;BRgdoQ`&TwFM9GXs4x6ngmx<^3t&kYh`+#u^wch!BFbF|0vX+<{uO(+m4sAW zq`HY9?4GtBdi`2Aor*dC4`pv2m*d*D{a>+=426miGNeoqg-}-JIgJR3LS!nbD7C1S zWgePIl4#I~$P}52(0~ddNzsau=K6h);(ng@ectDf-}Sja%et+)uJbz2GAK%)&?eCF>x6`9Y21VK{2VZ&oMw+U!7U_fgLmMUl) zU%wXg3)~A^0J$J?^P3D|pM|h~RhA(RbmVYlD>#hr7u>hiz2Eze%f_K4d6zW`I5`jHH9u?5WUdFqOMqv% z*oE%pWasp817ld5v`qXnD`I!EMCQ}gAMG9b6McA<2DMqIEJ_^8O<|88Jqj3afA+uu zcDqb-(Hy#+)*pD(hWEqQ>oi9F)3d!rYRn%NPe!!u7I3R9*dj4>Xil2_NkLj+jWi%4+6b z#55p%{go)aQ7!R84^p?Go?!D?mO&&r{+)d>7(1o+Fg=|<1KmRpVG=a8(|ZDIHm%v1 zDC(-;tApJdZLDPJ!kS7lS6jfD9Tgsaah6@FXn|z8V6}z0UvhF2zf8G2!gz=TyEXI$ zBp`z@HG(zZoOG+6=f^#}Dm)9Y9_!S5+~F-#VSnvJ#_3Dmn6SUtv9O%1bOI z(Ug}dM1gg`bOtxl!4kAW<#qzKtIMV{IG)*pj76y~)jij;5JJ*Z?%nfjJjHk^G?>8C z-gmG#orEZ=b`>N6*koj9=s)zG=py#>|Eox(ig*pVaOp&cmdFuRWI7^tJaY?&*h3+J`zhB?y3-f&Jm2p> z^`|{Oa5Ev1WxJd$n*;+3)QF{h#Q*qlexr_(6VxHwedXP}oE%JC1DxAVFEEyTtEk9> zp{-bm4;gE}Q(s*SAjq{4lz-vd!YgIcl{0IWGj7_e$~3S>;+<$Rz5M<{Loy46=?B9Y z^!c{0HXsA^0zyIY!!bfkr{=VO=cTQrCtShkv114KyRq&0-_335!cVY6hs-Z=l3NcR zoTK1Ru{ZQN-Fk!|!syj?p1lZJ7+7v`aKP&Oqhdd%9TPBE_hZjW>9y>=@M9&UEyS&J z^N!QgIk@&AkX5O0;cDt#;+fRA!{GE_HGH~=g&Xh={>do;w4K+s2ScF zA8RINB)R&8Q;|(GT>LjOD5{v^mL58EsPHg5gML~xLez6!^jr|uUi9@e)H!mF5{VB= z;k`h+7e_rs1(d#k3t5koLLjdSJ+~1X^ z`Gi5|;8$l+aG+x#OR49}kUX8`UWJ8yuc!cO(Xm23LVwz{up4#*dN4GCeneA5=NK22 zGlpTK6p@6w0m?WZcFAvA#Y0D0Gr-oyCTG93(t-0~5R|;ej*r%+Nli~MisvvB#PJb5?**Ky)@#tF+-z>|f&H5voJ5?=95102rO%71 z5b^2tN?0Xu7_9z`-^@B9>r>jXcOoMqD8z)%ixt(2E}SHf(P^V~|3yNa)G9(Dfw55yPPA-K9uy4-_MNkN@UZL#eVSD5wB;k9Fy z>?rznj@H}k_*L!lCUoFKd*gZ_2zu~oks{~YR?sD?^6XTrUn^sQ$^X1LKBY4Zt-2tX(HF78^TU!sh>`$By)^SM9%JTl5GwCbO zwskf%&M8`;5TS08Kc{CZ1Gq1`TWxyY$Mx=xO24eJ-p@9c{ju{3o{_}K04i1R?T2Zj;+u_qy)}0g{7i3?m(OXw>XK(1;nyE8VJJrwG zxkzu;o1e40I*bvv-YQwytQh9vTo^mKsGtA?|3Xteym9CVjt&m)dnldwwE$W%f< z&;EMECB`vDe>5KN_T$E1g@VvqN&b`kFHWtTdM|GclSDG-5!>-ST57W`P_OL}~nnG?oi zn(OUk&(+mgzuEA98q^*5kuI6f`O^>f7$sc$iB%ikD0R0hhVymp)!4BEtAluz;GGn8M_ozFoUA9K-+x>e00CaTIJ?x84o(y!5>O zx`Lu2mBd)8(lA_PR-yoZ@#4p#BDb3IFxo$mW8SUHt*u4_+;Ido#J*?*7~29 z_N|T^dxV8PQ!}&gvwmMhuQ{JDj%){^0~Y%}E;psdZM`U?G3WP7)aI$7&sAymM61ml zJ@Vw7ygc~Z+AH`6u(uFAu|BYHEB+1z@#mlC3(JVUl$(BDebPk?NsudfjalJM9)zlb zWii+s4SPzoG(gRXB%3~mUtJd}9+GzpZ>N=Mjejy>>;=`TKX)!lv%f-|qeu9`6krEW zXJgGEo%3YX)TyOzQ8W{<5c_J;8&A=jy&xqxY9vgh0fQE_Y@mOjD8;e+AbrUbGy}v! z=xpl0k4;aAaXTX9IN$Rb+)m-9&7+QgwTUelYPdx64oH=ZKKp#7G{xb3=S~3tbK<`SR@(sGl0{cGzej$XbBOE<3DjTu zAt$Kh=+a1A@cA>7beGm1;%LVoAB@+=ohA$V?)hY%k`x{U$)TYGR;diXz%HVSC3Kdf zlo}Ny9Q3Z8UcKnQh1U{qgy)~020zsmZfNT2k2Dtn0nE%E>_{QFBOq7@7>V}n-?DThF}G+C$yxtm>Q9m(B0xI>3QJnxpRIePPlNhh&Ks$ zX>CO=1+;H1`RB)l9&4?v0+;Yyi?8*EHWN+>*2fP(gwb$uywShQc&d6tjS7yeYK7{? z24q`061u`zqIUb$ktHxnr+aq<`cm!*37V*rfn2q3j*$%X7`w4|nmX!0Py>z6|DbQ= z*d?1=+=^2hipCK>1!A~-X6%v8lW-sonLmsUujJaEb-a^)X^F5RhFI z;(l`)((yCHt26#RhM0WG;>GtKJYZQ}=*=O@kD|C0!7D8U)|C5(zLHQped<)GD(XLk zj0ZIL(|V~N^6U;9jZJ_};01Ww+L@TSGC2jO3>NL@X^RfR%CRzn7%F0!;=wTpG5xof z*R^ZckgzdGOLb6T@}OJr3t({HBS*$#CxCAc2!fnm?uwq!`xS%wS-=h&@*Se0ixZy-Eo<{eS0nMk8Oo$ z=y_YL)wLNPI;{L!ruTxlZIdx@dQ?BrhPZ^J5NhgG@|e~BXaLFk2)*~~U$yYgJVu`$xLc z@OSrv9DM}&5V`@jMKpJX2f^-%p!&)4o#94^zTQzd;&nwqF(-vwpxP7>~H zYuaQU7w}+kI}aneG(4bUGQ@|@pqW_Se;UaII#yn*$&B4R6vb|qnhZ>I8k}kSW8&hH zxF3C$7K9X5o39rayl2H@*TQ1HB)rq-L*xpC=Fb)%%P}r|QK8=Dd{qbblEC!# z$Pc^W4?acz__L4eM+u#qQW88rZ~|EpHAm8c(H?MaVmU4M3L86U?Y(<(?#-BV*<}8yD2Fym@qk%oQ_r^SC%KZZOTs$mg=&?=5%&2xv14fJ4+?q%bjPEE*8b_S#t8<- z_0IP|9==oP1fHvUK_kTV+q*xvY#AjX-fkelr0iOBWG)IU+I;YQ+msgiWBTSF+Ofkv z@4i0a9zxDmv<2TG{N%iRo4A*K7{lxe*mdqpD|hN9)Xm_yFMk*mrzZkwOel7#;2pSN zxPQEZDe?^*_bV+kSo3nm(x}2?JucoK`0dNaWfQl3ZY6;Y~kB^Un zeLcJj7SBW;UkwZT1;kv*w*e5q>uqcx$ld57bNq;vjC*Fe`F*Bz9w)3<6=$}SPzHKD z{1XQ+ji9-gH}x%>d!-no8O{Kh7OG=6)c<1h;}c!6x`3%S0$LuVYQ52mvME(=VnMT7 z>Q}2l$cS6Uh6_$CxHxF^p7Qopep#D*tfuep5fU0&LC#5n$-M;?0$#Utz+OmCg23C) zn$FNNBGRx1OKu#V8C#KqV+`=4G!NG08fo~)ppLFPNripCQ@lg%9=j-h*LcpLfrE?u zl%K9!zn-qvd(!Xzx;eOlIvw*wZtc8?t{Z<15HF(qiK-!+Oz&S$QyM*Z@LBhq$9^0Y@PU(MyEs1Ce3w`9?`}TD>w1U1Q>LkoLoWxw_QT`lB^fl8%f|KEWddQx# z{?Gi?GZ$qN_qYE(y5K3c2 zQ$tlg=kAZ&wYA%Ec4I?ux*qJQwYTcRHhcTgqeg}J%@Fbc7g5yb+YqFQhyrqtYJa0n z+>)(Z$J9ON$OVYOxN~G1&z)&bPab&(^?y`p1d^W!XclL|AEX_dCo&|$-HFWm?( zK>R7@gR^|Nl!~_z(Ix~o6Epi|H#~KYfMIyit8uBbi`IlL7%ELzkrloW7O<94VAVBl ziBn}ebXfIINdCFqI^*hh|IaoY+H$0F9?3qJTe$IU(j*Us2M=X8+_U2Bhm)a5Mg(=L z8mT;LrAKV+CUzQzX7JIZ43?M+AibjwfhLR5M$hg5IYq}E#|lT+UR}+#&-x+SM-~GE zSK*)k`Ze>F)7W*ufBG=sr|~l71xCe~W`(gCgRwzZwN+((L2iQwkEPQRVut!Urw7vZz-_r34Ov>*6(0#xrFC4km}#8aUXv z5baSBP}ruuqE+kGUY&Z8>}svFgI>-Wi0pEdw)Us^E7)?~dHJ%6aRI|Rx$-WH`lD{j z8#t|HNgH%u**{|JwA<4?hGk`SYxAg;k;071V_DpB9xARvQWC1lM-LxYaRI!f(RKRr|73qO<>%Z<*6@mHC>3e*0Si&qJ}`*!7np55k-nSNmUfq(w# zd0>0q?z=~)wz=0e&VBE~(9>#)=T~$umhNv|ol>hq(rlI(oVrcJT9P;u&sDszX!XI16&BoW;JIwbD9{uwI;! zJ#6$9@4n}w7r5RC?&C7R&HR?rvS^utVUzAJ>1?pxv<(GC0qmLJ#ep6b+C*MTc6%D$ zzO47Gp53}Z9USi;y9=$E7<)w}kUTyS$D)*!({Ym?7M26JSj0B>7lna7xtmw%7{)9x zXWI?Epbw?<3lbTQWSQ~@Cj!@xnF&22m;3FuByC=4ZH=3bX?Br$a^1hz@^{viA_`?0 zZQHVCq5TJmyCda?I`FGo^?NUpp1RN(Bm9u7P`rH7rr2oCoT0($rBV`!WODYnG%25p zovLn5RIK@wGhz3a&*mLt-s^@MgiEx_#p*gAM zyB^u`EF&Y*Fl=`EPq3bh%FXHz`&MeC>`gH9I&k2?hU~~}&ZpyA>s1>kY+BKHp`EI# z>Q0Oz;5{EIy`UYP6qylgQ(fD)mdL2lIF7#*C)&`r8?lti0+xO#?Ix~KN&Cw5r)~ef z)Ouz5$-UQl2M2hs-yc7J(77R{+fBZ#T=V*(N9L3+#6Jk1d-?fIWa8)@kEzZxe6Tom ziE<(&DT)j4Pl;;%z{2PuPLxvk=lqbf!MVC2lR+cG> z&(3-A;>u$4g4M2%d+Q(Dm_OHf^Vq_L`m2x1r5HbWm>W0$!t738_aD#gq4i>GpjtR! zDcyh66lqJ0;h6DJe(O7@Q;Y;RX+6Lmuu#Ic;lF+~@Wx|%Yk;%{ri9w&7o?7u*L(;P^pHe4n zW_4f05l$GJYnx_@Z~fDi@haH=6zT6FV#5Li1O^JjrZkOY<0|LTni*lmm-oGMUHx@L z`K<98MTOT-T((NzK}N$mvY-hpR_LfOYTwVon=#`YQqp{vig~*G93wJbyf`^-%LiG# z`om3EPZxeHTDl~?j#Bi26JyOzRJo=f{}MLruL{N9MZ>S`vUu`g(kAD+@%GEp)11?a zobzYcxFip9TmEA5?noe|_J+uxN(001GzO!6E9{?3U5Uc<`QW4gZ+fF(kKWFaT z6A%H^)G8LQe-8DXw<(!(+I#tn@P%U91z{owLYM)JOP8L|4>_Dx$eXZi4fk_Dpy2Q< zK(Rq*o^jVQapxp3WpWAqCtk4F3nW-=Wrb2FnW$6NG5zslZ(p~6m^n_D1pAq~##@Lb zs5q1Jix$pPB10Hq{*#cE92w!}-J`f}?9By3dtC_(tbEYd?J+GXoSV*VI7n!W*CIH( ztub4gn&1(&c=cxWw5~C$18#>6n{y@5x~*SVwH+771l?*kl^ct!7#oe^tcXmGYSK~S zmJ5N3g)lucLTYl-W*`N0O=l&|fwBE9)<@#@>TB+fbG`SY5Rs8|m< z#%l+rw~?e?VS7SZU4dTYcyYeB_21VbS;e-%Ab+$>^DlYIpLv@ik0AGC(JJVV?4Tl%pOCclCcA?>C5Dm3`U4&?T_pm&LHsh%zO{JIQ?Gok z>70k@N=|m$y!jio@)4IXx)dTOc6;#7S{p(z&Uf|2=Ym-uST;;xa21~?TCrE#w{_S2 z1Yt-a8wQwBKo7NUAw!{R#sVwmIDd4uZS$2c@xE4L$u&c8h&dcp|3=%QGO;DpQEDMD z+ZClNt9YC|xtxqfFtxU?Mm`zTL1H}*NgY12Fzv_57tAm!aTTn)wYAC7e?%aboHDh( z-uXCHyy$x=DIZ8b;3O1=7#nOrNF~~13+-xJG_7wCO_CUQeJn3Yy$Q_R`7>t?P~pkY zQ=A^j_2p^g=5sWMiuaVvo~#sm=MzjT5x=Uv64dx^OlcNp0MpT*cboL_Mh5_pPc;9 zQFVElc|`_qpPVSJOw-V_(di&j+lko^_cDoNYPl4Bsf`>*gXj7mez~e;0eW|G`L~Tk zKACZDAZ3E|q}cxF=c@J^wel{<3n#$TE+PU;3_gw0K>9oGQDM2Ts0bK6m~|EF&thP< z;M+%!B7R-( ziHRc~-EF=!wQ1rxlz%KMW2Vjz4j*n>R(L!nSM;m-{<>-2I_FQdl03XeuYqV4!XnD7 ziz(SS2!POaw&e@lnt+v`u!#DM46XN#oA54|- zg9dNa<@d7M@s_EhpyGUwbY%r-kuz%8?=L%=FFQF&=mwF#pU`m_5-UM$z=9KwSlDb%Uwr+UCV%cyY5Gy~SuN7-}d&7=3ac!6GnH!;9$E;xuU zn@`L=I`@1JpJQ_t&;LJAsdam^3yRCmWDWq!5e*2Sg~?8LCl~JP>N{8UU&RC1$rMlyI2&a}(rbyu}d0v5L?#TR3PuqvH zWK8It=;knIcLZ%3>WLGjs;a6G5JPKRbSLv?+T+J3a2(P|u0Ji_%HU%r?xS#mWgiRwur$YRim9<$qg z@@nI*IVZ%Rp3uYJH9U1|JZ9E~;z|h(*u?IzmUT9bD&IEPYR-M65wN=$HLlee;Ig@g zERox2>eRb=VK*sIzC2*HM^>()E8!er=`(J1+0D8UJl24KoK49M zH1z(8p?l59@%aX{HtMNxaNp&ysyHM(B13A;)Z}YAUQzcol2+k*%NJV=WC6!1D{8Mv zTDttz6c0Hl$C@(@DyMM0v%ZUts;z9Fub)1N8@9AHePDz|PGV{rYXjz*rW7}470w1B z`krv{#!;s8q{Yk)jDu{YWIR7mgrsd3eueS{FCJwrxSN7ec}WG;F4$%QY=C?|Uxn4c zXu4$~1|NG`d@~}W7h-#EmMCg^$PrqxJ9Hx-gpe#6hEz#l} zU?bJL$&+dy$7Gm|ex^6ZuiEs~El{eWJN4&ogvWlP?O3>cn4^P( zf<6tyxb=ns)%ZBXh*pj2HvX-8Rzfed?{;>bP6zqjY&`OG19rBP_thxMj!6toTD7Xc zrSTHx;f2Hltst9OLmm9O*1TKitMq>Z!}^iPrbsU)_#Njx0r#iu(6a zrI9&cnYbNvAqs{AOfvrE6awdAh@as%4bCyzEac*PI0N#?bmxa0F$yyG$oB01O}Kaw zyS$x=7ruP{OmFOC|1R?K#&jv1nBh>IF$l$dQC`Q+owc>Jo@kW<0j)a##xBa8=7OqdLP2?BR{;Covk%#(i_D2ZcU9;oxsJNWMp84^Et+J8-8R`0s?7|&s$9W zn>G;`8#rhFl5G%8tnXgB-KS$f|3gxb)s#RBK{`3+=3}$A_II-2G3!g?YdLT5u9XH$ zV%Q&KCLcwyqQ>t!&w?^d)3^M6f!@Az{@L~aT0q8^Az!+`7YQi+>=x4BSx7s}9}TW) z*Goz1#?FRZ#?iA;4fjzxf#&v*<5^GI_SK(nq3Og@2M0yVeTa&^xdeVG3xgb+Rjcd? z4=Q(o6fiYWxlQ~!Kx2pp&!O_$H?>@KW`;o1o+%~o5z1I4H^xO2-*!`(%FiECo?seb zIH=f~X`E3}U&y1tp0qK-ihIv62{%v~J=#b*Br0kL2R=v25J%cJXp8;{?$zG|&K=sW zl#Y>0w78BLV{C3-2c`u~EbLQGc?_U4(Y5v>CpP;a!X(N!OLz6-9_Zy%riRV-p?-ZL zD5xS?v9HWL*@wf+yzeA8A3PER&Aq1g)vK@nL1Pv0O@9wrgu40FZKXpz!nqr;)agzv z(dIXSDH(97H>9u58&Yq(`-4?gLF?wy@0q^$(MYX>QKa(_E!*m*cAXJ4=;p-`>SkUe zzDDH6xX9TP$j49U*rCJdkt3lPbaaOF;z&?ePJlQ$;iVSbSpV;}GVcQ!*Z6Xphx~*qp(b{K(lm4iesPH<_9^Hiuv${{xRIL_b^z*j;+pGp z46|qn4AJL1cYiNG8+4MUv}x6<0UqIt;!jhhNLLOB=?5FD@ZeaLiI*RSy{id zLVan^)E)~=$gDE%lk0{ZARf0@A?o@R7+zqSnMsiIDd}?t>p1(;oosS*2o)cfZXEbh z>xwtC3r^b0ESW{}&fZ1^;7p3kvzJj}m#UU-7m2E$M#bz2Ytl_3jTfdH3*ec}S8uzs zrf}H2HGfrAQe9OM1m&`PEYG#Gi9-ziRu6!>k7P8gHN7}S>iatq3=#SO_m?%sR)dFzE#o5f*r2E2NN}$ zakBDWvZd>U?q#3G%~zM!n=O|@>_~E`*I2!Z!%AFczSlPO@4C(OK*F&1|NV%Va}Ds1 z-RJc1rFRK6L#jUMR2zpF2;Krb28uGALywtU|Dr`%*X7^Kuj@ar)$yhgHsduyj&6$? z-$8{$C^LNF0F|kUI<^s0NG^1j+9lQNHPt-jfbg#P^f8M+Y5yHJZk+du=Ip|~zP|FM zMaFba_^?iew_XtsU5V6fito~hi#Il8411{GD!qN1Wwer*?y~$E$wSxyLfo8Q+)gKI z%oNq(8y9zdWYhV}R+~lG4|eSMLo2Aw-aq-*>eC62t0%{-Wg=)D(~+4#u0{LBp-&M<-0%QK19BktW^JUtr_7*$W{#s@ zUm8{*MqJP|$_aaonj^S|tTtRGxQ^5rgpy12zbCQ})L2Tro3e#vLjKx7$<4V9 z$^2tl3?b>Fc|N}~Oalj^JbHc`^AH0H*(4yMaWhqu`1A$@9717gdln^D$I@JGPBdA* z>Yzb%WIO{?OLyth2VarB?(PzmyYNWxE@zw|#96RTPEK&>6f}vhNu2t`QUJ2h`M}qB z%E1Bi_=c6MXWdOtAAyxQi-m!*pE+H~H=0NZ__ua#+O$`J6X8JFY8{6RJklg;Ix-o5 z|5w~KzLtY1>81Gs?}U|Og4HYrY z^}n)|$@BSAY3t1(@9rP!n~WnU0NeH&zty?VL@^W=NR-@IJ15E4?udGDJXtJR>Fthl>}4A+>-kc2&XJ5|6;QS27VePZO-AzfBf6S!=Rb7aF;#&&p)%M zZ!!CbU0K7)3k)e5mNL7iwyhIj!bV#oeT<|36kB9siMl(W%IXt?_ z;sH&2JgVO4FM;=z0)950Gw@=E1drRIU%85rWp61AQ{2fH?zG>z5?N{K%?=JekE~L~ z>{kkc47xGznQfl?i1xX&_G)FT(|^-T&MeVsos>co>(#Kyj zXJfF~Chmr~fPR6{rufg--1sVvj{Nr%WM|>Iw}1a!oGG85_C%ejGe4v=e<13BG~hdG z%}>#nj8$%AZczHe=T2G&Z!^-`y*3F;+q4m1%wJ|{hasbB=x9ox4XwyOd?OOXgnJnH7Ob`J{0d@apxvX@cYHgHB)$#<2h2UGP;PQsiK*Rf)f|}?c6!D zUsROjiMY0V2w<(jt|I3VRMh?KYzMw6%k4KibIZ#eYU_HeAm#i=6|n4_+bmkN=seI7 z3B~VQSG`#fTSwpkSk!_+(Fx2ZOc69*ZaL2?p!7Y?>D!9slaLVcoklE9;J=<3H@cZU zlZ46q@5Ihb^gSG&4L~bmYQ-VjmoaZ3fjMu6-gx}*p*R&ubm@jI*Es|FzyfJ47-Y-p z{VDIdU-&;!z^@9zAE|wY&l@a}^t4BdKy?wN$A_S4i&XBiyJ#L5OHPEwh0q1ycdaS( zgMX*F2JE_nLG-`)oyjBnC{9iSTi?WI1L{+;B%Bz58zAd>sQljRD@mzHI)*U2yehp* z^IB~icJ?f&(<~Q=wv8lM#-W-O0kSaqR!w?Bvx{Phy#5|S8XrA<+BAS$1gHfppjOS! ziWjqy5DH_`IowmN5>ix+w0Q^yeQ0fVM9}X!JY_c7Q+aMSRX~;u z))O~ptP_{wp70fX2)Zy*G$g1{h`Os^xtXL3?ZU1HJRVF4NQi0a%4EYK+43Gvk*%7% zu;|Hgzox%PVc?K1p)%nM*!{aK(Ka9~tPqwEW#16vaD#B~RLQ476xhVwOM2UN?_B(V zZtrRX8F#LQEqdsqAF?QZr5Imldjv)M^_8Ki*>!up>N39%%UddShL)b#22 zCQ5nUPyfQ4fok48Rzi8r-3Edrg^pah_leN=PS&(L2vwUJ#JP%O+#}ApQ)(@4%IqYBL5EP?gC@%Ivw_cHoydHt z5;)%PK5%+5*^?-Ts>u`0-oAaCF{TxP5PrJ7tZWn2mi1ZTqsR+vXF#!Q+^k-SqhhKe z8c(t|q*-GTh9GtuGv)`hLh3YAs!zpVzv79ukZgWek!ddB$>3jf3FFP@5PMCUD$Qc% zZih4=e=jof&>B0o35th(6_aSyoQZ{|eZ<56Q{*T>E%%>q+bfs1k`$(sDkNN@O;97xY%`&`>2v5I;OyXRD|@Y$sAbd= zvOW$PK70d5Ih4L(7gKZd9jpOrVot%jJ{4WFa1-cZSZ|TBRf;WasAxF|u)&(_rjIE( znS1!ZSBZz*uCNUOa}%j9(iOjYXo`#qNicaPDi0D0{99+3yWL>g zd(1`s#wV}ET@5zt0nI2CWow}QCegJ?`Jh6gt=@{f6NxL^vf6O2AF+yQ!+Zye-Io=I`j93y0~Tee0UT7u6eh?WB-~ z?;YAC+Z#wzIG@h2L`57F$Pl@9!6fhv31v6z(gR&62t^J4Q(VYoCBmlahK_wEjvF@Y zJUd=rI$-YEjs|Z=^s7AJWs-ivKu}FPd=8|JRJqHC`2Y>Zjbli9P0*SnkF59wlbb(& z{v2VK64s&OPAm6njy_@XSbDpqM)cN#og^d!gzoNUUa8;XP~zUpK6eHj3%O2)ftO`! zl35Pfo4Iu;JoH7CV~@xa{aT$2M>fvW(V@*+u0Lrm8)?bVYqp`B97*S2`-nbGq&~1f zb5#2_f0vwRcM+7JtYq`}yN zkPNYME8%NglbUx%>KUHVM_c1%0g-W0#`oy;u(ocmLh~Dmb=ZBHY^d{-_vtYkh!4VV zHj=u?^RC+%LI^XAo`5`n>eP@z<3y@;Nm~m3mOs@|Oop?yJ%16jTc#iM5J6jNYUJ8k zzJ}2C-dmt531ryx2@^tyxeg8(^A@lr?1N4=0~Pn9ejAR&z@E86(4mRXIhzZd%_D;8 z)qW05`AKoU9@qwuj%MT|e@zPlb<1(|RZu$ckb`Rwy?&b2oFAzha3nu{BeZ-st35#- z$sHCZPoI_nOX(!_)FB6qgGeA&**Q9zrTfP$tzTF?sr!}Y!u_c@?|oYrFIt2{OgvB? zY&QOm$Vb?utSp?>XN|W_>_^29R4t6zIh`=kKzUUdLlo;fVcU5~^HfFag^Q#Ye(Z9k zupJBCdN!&7D#pRV7AsZ&Z`Oqzj22bYoej`;hz8EEqPBN<^!TyyjjEY@UHdohK1unx zVXmZ8^kbO}NkNAY3QXrRoPiHe7fd)j+n9CgxT_ zxx#t9#b8WY<%#BU%4RBe1&P{M7qjWZrE=(N^e5#sFXvlzM#{T%uuTAFqQQ!2D@2%- zXNZk`U4JZu8kGBRE_i1kJ$gU>y6jZ^xxOjm^u?_`%4aAmPF5e_mnJCTi2ChhI{L~{ zm`EWmla+;9+_p4%=Q0PgrAuE^FEjoYgBeW+hh}&%*`|B%-mj5vIR2e<?Mel3wil3w z#w0w*U{={l=M5f|6c>{ezG3j)n>RNYJgvWn??j>O(!(ihL8=phV5gOv3$SWoR4$R3 zHl)X;#F!eYB{y)`CO4QKen@ix^NAV>s0#W*c(gA1j*JS>f10wkTb8933YhI}+s+MM zs2DfwR3Ow>EpmpRm6eka*Q19FIjE(rO^cTwUd5!RNUcm9Ve^GFX4#d^mR$@ZiAl_2 zNG#I*X8GZNL6TGBdyHJ+ZtUgCK~9cyQFZnx79qVQi_jfxYO?Og_UL<>_)-J!P+QA_ zoH%@Vw84YG4U2@@!)7b+yKxZR4bHQ19O{P8Y`2|0FL)$1i%lTi7L^?zLU0RJ$VxCRHw0usqeh*>&WBvkG4V zP!)d)X)-?0&JKiV~0c(+6aI2X1A=3rrSy4(C%AOAQ|uy|}wP6u({CEbY)@OSgvZe94I8(}op+{cfV>n5n+L{5q(c%Q!)jbm#%B-2QXNx&AR4jN@ z@Om6g5fL^BXNb+zRiKp5obA{gXsYGZ8>cwZO&kzSKc zi?;GYbh2nQf`ul_P}>)W^qRD0#KVbgt#{82F<8r(u8p7O##-#GnU%R%a<7;(IOA?C zH~&~#p{GtF-J=;-&BY;+`BAHR*{prWSx&B^zJaPJ5B#6V9T0_1?^D{?T;s46;?&Oc zyNAFz^7XxkbLuR^U%_B9W+&?geEoNy_e;*5i`10^o)|vrW4PY!q)r!@3 z_V*M2CV2D3UM8JgO;Fqm0WW|I=MtlJ*y(~657|wJJK~)ZtlEwjBYip63E>A19Pn9! z#$6xWNBytuWt+^^P`{~38`Iho@83eDzkp>qQ@%dE>~s0K(9lfidbTOY4(D~0`jqCo zH2Vl?(g(f^Y1by61bPPh1468cEE`>99E2&UoORyNV~;qTfAVC9p4&5A3Fizd9-`qI znS&k0>=V=hgzyepvIjbz@h*S(I=3Hl$ho~iPYC}{47kAZ+CvR)|9&Qj zL{Qxg3$vy`F4`ab{Z}va%AQyfr#6i`9W^$(kE^iV#j8TTgtg)bYMK&au=MKISwoWFG62%x0eFXN zn_2jhlQ-hm<8U|hg62{2fCL%`#g1tu=PNoF!|+u@jl&r(^zr@s=Ufc;Bl7pJWbD%! zW)OpU7rY0%1`*)65hG#@9+*ZN)3*17y9~EV##J(_<%^yghW^2O23!U)6qc3Z>7r)5 z$S7fwfMAYdo-J@G+gqjOn=aN@PCPSh#tcEg0t-oL*P*8o#S>3CG^*rNfJ9QktJCeL z2H?*Y3QGlGIXT`QCh&s6gBMX|Pn(atP|_sQyWh9`)ZC*A+d6HPU+m;mJvQH^psXzR z{4JsJ6LAmNFV^8dg@0FdS5i{<2sev8`FF*`CmdF!9xWY@9qNYWQ;v_Q&%NrfUMs9h zs`hZKs*LCVt$Y73g;9a!oL%}MD9QU`@Wte=@98U&mg154m*CfzU$3TYeDsmkpu;Mu zefWD~1&11|su{CcZ`!2cW5MBf>etSdq{^1Wt;1SOaN&?%`pP~>j)+kf#LlLh`-onWi4zfI?NcAn zJOY^=$Ua20umBllRy?d02E&wmKEJ(j4Y5|BW=no@UWZHz=_yp)os<-)CkX3+fW_4Q zkqFTAYDGUp@gLtULpnz-=lxhpIXw^T-_KEo=D|Y_H&^-xU{K}s=@c*F;J|Flv=b2v z1iotKFk`xG7voKPy#r0?AC2N(m(MzD*{Q8@9#_L$TnmYW)a*0zAMIgMuvt| z>)HHkSFRN5FSw#eVng8Uu6=u!#FYRZ!)%9>%%a%4h-mr;9e;K?3BfO->rA!_;vd>i zm(fY)gR?AEJZM`zmKsZ(8U8gM0PUL^D=^%YX8bMj?oKJ=3$YT}>ahiL8QX(^0C#S^ zx~~@~rzjNUU{XMezZxqV7^)?{^5mX%6;JiZFejlwn^$yt*8abZV&SmXtPjv}h)j?^ z&TJ^b(B2JAQ0*m$>4r+S9S_&7*x?kWC{7n6}#dtUmiPF zRF>3J;&Jt0nRaG8?pyDj)$iLem+ZrE`zVD1%WOKphMDgxdH8~L2b|M$acTk&jBM0^ zt#jPg>c9@tF%S_qCzhnt)@asD{4^=(P^lhmo$Adwp_tf2ZUP{A%_Byg3;n>F1=ifV zXHDSc_gP9yEEjZYjxTnXfR-={F!zb?ysWIPX%2pvmG#3l)srridLeMp*S~n_61t&W zRD`gU9$sElA`{oY#Jltyu_E;y@6Azp8OESrzf%v_JXWWg4An<@sz;s60=uM%{GR~> zreyvjFV6f$WIb-wodJPK|urx>uM}F#%_Fz+z+Z{N6 zz)XgT5mVd;u(y2a(y%=UzRHt1WL~v$t}HPD-orcW{qFVw6=@ALP@9grTRu#c8bAhY zuegSdS|0lrqc+7#S8oj&&z0EKk$u?TmYua=fd$>_oPe^u$E~onJ+b~V&wIE$zETv$*=&f%XS$Tp7`{~s4hR?zGX?^C zBk)iJw2b&&KBMswIH{mv!XtG~e6qQ?1qsN)0;w1!%u8a(lL^oor-0psr9+yZlR=80x6m~dxNd(% zh6W)Geq8jbHREoGh+DUAB^CzT$6mf%g--i?bhM`&frQ4LLLjq;9Qe}E8rwLN=%`f` zKIu^A)j%6QScZu~cH6eaAi9dVZI|>qFK+@V1sM;yAGn$JT*Kk(q4@~^q1211r3fV| zyMu(8V;THOLn-63V{*-x7g#ce47(+&s z2rGpFb-cgE$_kG26~v$FF>GvkF`|A%|Nh7xRzc7Jn4O=x5VQ*m3Mx(p$@>w|G4JE0 zuS_@%)5-W56uQyJQb#AO-0eCM1Qq+ahhESd`P>~bzz$#!=41_qY(M7hNrdI@OLC7_vn&Gct~*6_Shq2)>Z3W5p2Lapt^v zmEXUEJe6oBMJ*1EjP8yCf`P#^2oI8hY@{&On_~(r(O$wJDR%FU6nM?bm0f%Gyefca z-eq_L-pgX1o>!|OK811ph3s0>dM_husQ;V9Zqx+>lp)*&%R@a5ZSoQ`F5pY3SCMmC zKo(ZbvLRis0zy!4eBLINwgZ)Ck`fstrqxm-40xss|6LqMwmQ`~sK zJM?%9c)xzrpru|}BKi@l%02?wr;VG^!_<-0tIw`WBX>OWJ%b0A%-KTBn4+Ifb#_pL8OJ#B zw?kdqIbA&Uk8o#rHR+g*pJkMt2g&Cv|e1x)3X#KD78=k*7X|HuGoazeruB9BN-36wjH7gEmrEfCH4 zcXOOFqsn9*eH3sTi{n>%0veW%a9g9kAUbuL@=uaB@F;bw#XMV%BN@AgY$uCci5ASM12b9pNU<9PfnA~k zW5I@m{E>&9pOrssAa@*Y(S zx9274M0WNi`FUE+5Mr_tvs`Yl7YKFJCOiW>5Uqg=LT=0GAnp+T+?FpS z5=%qo&|xK7JHq%|UTRUtOpLMrtq! z4gKIxju%d%sQL2_A26Ot@doe^pRm%11AzOjP+J=DXHM)L^c4)ZHc~x?ZDH8E(dDz7 z+FjiEMyF^5kqd|Xl78dfiIp3uDj5&GF#bXO5@58K7goG8WoFKui`J`>9ZQ--oysSAZQXnh zDc{w7KJ#|!L3-VrmuUDuAa|@50h1sJ@IJAZ2#8$+rV=0Ua1gR*W_=E<02#260_FOz zG07tJ0Ggs*gH%UD5tgv5@*n$IlyE%c#_!XnmX$Lk(eNAFzdSgM$z_S$)nuA~5J~FQ zeC!4QrP#{7>;BmB{2k55#Q8nvmZihK2RXll;WB&+|weWPzvPCjR6u#~5bI}+I{-8VoF zw+q5Elnn+`_|@J{OA``rGUnm$Rf19U;Py_*5M9ssDsAKfnAq?vBOP9yC>Rhnd**!b z>UhUOg%hQ>Xrc$g)z#Dpt4wY?%nbKWFf-V}O^`LathwCl1}K(O!q(CLnVHr;0S18g>K=+u^TwQUGwt0J|#ZO(*EFgm%dy( z$5OrMdV-L(%{=3aE@+O*RH}XG$J2|pvn?a`tF!A?6KH8{Wto5GPC*3hBAo8tE#MDP z#?M7JGSyDmG{nGZ`*zYHst-D`o*&Csu2}I6mY9~y3uMjE8yjB61ZC~y_<}d&zTRL4 z1A~Er8nR-R)g7l}6|uwK^0qi95Og;(nQ@x#($I@TO(K^@7#0Re}Z$hB?T(|>upd;9(u<1og3UO*8m0)-!sMb^_A&a#avUUxmyx}=qA;-k zSDEK3puVnbgjJ}4FlUE*Qg$2Smll@O+B?)D3IQWngqBf)G(6$i$Vp^b)OY8A0dsN` z%uGE^2wN@Hv#(TM7xU8y1IW>|pflK!HbVw2-2O#ObErr8YnLeMCeF^Y@;D#RB|rm1 zix_m7AJ2B@V1Z1z$i>0?ioK6N4Pco={w5urw89ueW%g?jooGa5IxD9=yv;0e%)zk) zkbdAhsq93p_RHHzCC6*ECpB!?UYnQFr||37i_R6>9X>W|L?l^vNht5$9XM)K{KH8- zl$6-l3h!@W!OW6CUuYP3Nb5MaV}=m9pSC@s*vE~m=y~~Tb^~Y)#&$dY$JPa~3M*Hw zs{HZeOu)oJTsa0n;+Q*AqwPcIK@)ouIL+HD4EO544`t)NeTYg8(+$o=Mb#liqe}u; zPTd=hWqll8o#wydVl_8?o3)fKBt1kD!ZdTNgr3i*IpCUGQzH?h-5-C@S35{k6VGoe z4s z*_vl(zgo!aAXZ4{Quw65k2$Reh>wFlp1IN8yoo;@BTU*{gga zo(5s)(V&?i?T_Tr^Eb%DAjoE%8~ckyl#3DS3-YwLN+Mk+1|q?Yb8i?R0*&a@`}$HD z)+uMm1>&7=qm5A{@sT^9aNtHXIUH9+MTh%IQde3~V06B$MIX2JJ`ad_>qZwBuOxXL z!!?}_!U5IKJKF5C$rmD8Eb z!zA|-xt++PK&<3n;|DvKoYkS{j`dhsocVwxn{C9tyKYyw7vq`4LciY1%5EeH;25&> zIN&Yh>rMXP$$QG76bTw*WBB}SSzQlMpkU`Y)5vMIeR(l2F#BbGzD0`yh{SJD@X^SO zC0@Ad*utypN zH;Sy3CsqNBq8w){#T!Fwf?s(~NA4e z>7(s3V7k==s(p3Tsz?g&;ldt1fA$3WblGo*dW`B%51|z~H@JjULAql{?7{(1v9Ossc; zB6+vTnqku!HTEK!uMm;Qp(>`L@Ub<$v#&eb7w_vgsCekGOJ}5;56vuFykh{P8cymA z?k$_-x7o~x#uNAyY+x}hg-NP^8Ae|VG!mqLWM~5&Tt6y1Tx`tavw_XbT7tN+V6oQk zrW2pV!()@td>d~CGt*yJ7d?bd(`oxzLoPVHx-P`pECP{Jl6(0aV9T-WnDmXifa!eQ zksQM)24Dw7%`bJ!5q3Cx;OWDMh{5|-wDleedqd*QU?GDIwi#z0fzGIm*f-9``VKlg zA0I*U9pnooCtkE9OuIqL>Ho57bp1+6zg*}Zx);w3?a8h)kUhQ%?MRyO39(;UEK;py*p99d$ zjAkk)Pikmr@QE5u=JkZs!syu2$ro;-kRXD9-gUjl99?Y&LR@5KrmL3$2kOpKYBk*(+6GsW*&a z83Y~5A%XM%@O*}npyE0KbY5or4{(s54QREdAJcIr>z*NSWxdCXkf3zXoW+U>Us}RHGqXWe{f zX2QeAPk5+ff_loVgMyy+cAsn}@Zikga^N@Y$(W$ddv{sPl^BfO=oj_|s+t3V&bU53 zdkUHzy^&Ba@_nenQ@i3F7?k#W>p``{oM3&CBXAz^m!j32Je@#}jEPBi8*u!k)tbYH z6KQ^?O)E7Ws@mV0YO1RTvf!=`)z#g}SvTl}?Ay%brbepu&&hDn`I2$3W_JdrOHaG4 zp(+VixSWu!#wtnM-%6g$v4weWAaHA}80gn6o`8 zEjM>N5)~*i40(t$#I|Qk10`=eP|?Li5RDk^K&Td$R&C$01J=+HZpGt5O5L}gKNaB-KxCS@pd?Bn}1J7Uxc*XZ`M79zPISw`e&eCNG@qjxOlvd5Kbh%3Z_-pa#2t z1*Jm-8e)3h2|WyC$kF+oCyzowBhi;0g)N-QT{kQ@PFzEe$-ndsm`gAC^{!7lOv88K zCRKaHIWF{75USZOlK`hAD`zY8-+$ZJUb5~U>w7J2BsXvF#4-daoZY{Ow3W5i$>%TL zS$&dth|FH)F8NRSUPzs!(9lr1T~g_ji>J|d=OK3*EeSqtHB{Ns3qi& zu!m3>)KD-nUyP=X%VG!8FTn)ec}1Qn@4I)CWbMtfRysvD?S~KjJ*p8dgLEIJ_=IIq z1tNQ){8#d^>v-sC85O#;7wOwbscp=%{ObdLuW!=E15ajTI)})enU-xgqHe&YxQ|PU z`@8<_-6!67E(vWFng_16j*NE9@m@0f#P;a6(a)Onub9xlBvDG+`&w#bE}#|; zQKY)?xgB7z#{~6zJ6v?BK1YyQf9K2tckbrw{(~j!FmX}q1Fou3^MZo0(b16K1p`gn z5xE8d`K&D|&Kw>mSq=|V%fFY52nw{eQp7S-HW-P8&+YO7VRFg-l2fX$N2GP-_+a!K z;oO1{iQp*Mr5sM^J3lViG7cx;-n_wk9K!*Bav#{RHC~gPzD<_4T(@pXD|IP@cFMGH zcy|GGd8@A0kI=@DnMa6+?-rmBX+J0%TpME$(rU`_Io*rZ)2NPNUO78Qts$qRwDbnn z0#wKk4Dw1T5X<7PJnH}}u4&b((Gkd!1E@qBKYIcA>S@f`2_KL7lVcO7#~+E~{2`@Qe$zOM5+ z&-0SR^OA68Y|u{2HU|7zNtZ>OmsHjel0Bvz{AuNufnpA4256Q57gfTljtko5p$v)C zc3xz;u}nbx7}0GOttZYuM&aUwQK zER8!hG07h}DO?d$grr17+(}=Jl4Sm5e)zKW;1Pmh&EwhT47@)m=uA<}*iX9-{3Lvt zab!+GfeEB<(rHoxEw$D8Fta`*%fksRC&k)*$r3*OTh3_m(ulw%exElli0IH-+C4$=dh zPfVi<>%y3N{0c%yhEi{vfY z2$*;XE$&4F2`Ed;^%f|RismA#oZffmU zWVeZ(Db>_OE@R+o7RL(sqp@)@8IUMjBMfFOfD^q+k7pk@#V8?a+Jdp%224Q~KHhZz6Tw$cW?>;PVfA<7Crzf+LVwp*Z7w`1*~I(zU;;#3|MODuf>O;mi1*fBQbNiT2mtHp`{9 zuPx91EiXk3^xH{l@8oJ%@X8b05@RY zYFG6g|6*Sh*^4|_S3|fyNo-$yF>v6yRd3)hFoVWm&vCXX#Ctq^`16EOaj<$(vQecb z{2_i$o>JR2=$_|?+m@fr#96k6qF3M!Cro(Ty7|wD_5c z%T1&CRkBx;41qvLn@=R~J}?Bn3lwy>9bFHn{la6C;N-BuLI_@}mEs$Rwi?z+c5y(|ph7#8bx9B^& zT!y@*Nz<(xX&Vy?H!&fELvNAstflWw#b=v|wRlXWkZ**j6=7fj495{7dM}_lU<272 zdu?@@JOQ&_J2<#i-~b!x#U!Pr1vfm8RFzZZ4cXpVM%x^+{VfUuH=%|W!x$z{rnGzh z=uv1y#DOD6m?khl5pNflMF#17axo)!NLoNalLic<-HCc#Nyaia2A@VDfqqi}X#VCh z1>#ON!^ZdP1TyJ64`G%>53*ZHOhLJb z>=GXUI6Conz{WSDNh3}{eIz+2B|LoWn6OuX$*_M!unwQkw)cm16RWB0bM(r9lydXA zzjGE4u%TEUCtWAG0BHjn7CkuvG+-ORRj!8MV<1aoSrE_|JzOljSQ?dNwDg6~h=Rh> z2*Q;x3qd8pw&#VNRpN>|ZW7f9n_<$OQFO-j(s&9Qw>OL1<=MVhD})9BmXt5>f@gJV==pi44^_0*|D=Iy4jk>0?_bjRNWU3~AX_KkdW3aO)& zl{@Ef{yWFNvLK+8&wM_y8aIv~JKM>rSMT0W%SulzU5KNF!+A(lZpO1xuSOX*F|In` z#y{Q~I653->#thdZgZHSP zR;UUgE0rv$EeK6=4a74J%?Oi;}}c%sf=+}W~Ab{L51~g& zGBPmmB3G2J$9U=R;*r+Lnm8(aVQPAEu?27RVC;mpD6YzmA8u-j%AKDjOsht3rIbd3 zvP*RVXbr%fCgaB_kr0Xk&`=eQLIb*QKprcAT3lHs5Le0%aw+vALLe8oqdm-DC@d7^ znH;EnLdLQMKawoIC`mp@ZUQOk73RY=mSy?kqD?6u_w|+$cBTCVP!|@7z zPA1>P-vYknq<;%|4iW_q>S}s5^%7rmc(8PbG^PXfQbKBLY3-brjiHy&>|X3XM*3t5 zdnW2Xg{i)t61liJbq5Z6uoEC<5lHBDr7XF72iZQvBudaM4NWH0LvRO-DqV|VviG$*l$nausBS#wZoOLKQ z1YUx#7C9l@ym$=Msi?RoH!!F4^*WrKybNLk^y(wd+ny}xyw_gX@=0t~GcFMepuBD1 zSm64zm1NtqIlCjgk4cAW&(477C0wb{`%43Elz^TAEs06xRik@I_SWAyK7Z`iw;CJO z&KN$4wblIuu|6mS={prI9((ui-yg%LW%?(zB*;9O37Pm|<9mgLro)E4q`gB2DQm)u zREbTeX!++`x5`qNsIF@sC(qETYYK^+sS#vxX~eYYF{=;E=pYHy%}>5`;qiiMw^#b( z3l4=FByU&{741~6vpl8qDr;^h(>sLYEzBSEm%62x9IjDik&&ONbm@}Qp!^a#vV6VE zLDJK}5Mya%@6T&38udqv?mw|0Jv=^3{g=Df`wS{-Iko!m%RNbJUbazm#vkn-45LWA z@7d3qzZWvR{F;_ASRld039rG7iFhh(m?@VD)btl;lBiveQvid7<6XD3i}}UQ()LE-ZFAGZ2i$AvyKqJJr1_Bw(e!-84jwY`iT61CmIKHiKS0h zPw0uFv;|e+?&%bkH7kk28-z56lcoAOgbXTc0fzF+m0vU;*VMd+&ERFUhSi>x5j}rF zQ>Bli0wXY+9@*12MmJfEjdX?C3g&IlGTYgKA$_WHs3#(UOSq=b2!?VJArxtRHaqX` zUAi!sSU61J6p;pq;4#8`ehtSKh`K2HQn6bM;%j)ZKw`5vze%G=2u!eUAP?$)yDuE_ zxM{xt&+Wi>ft*;OO1kkme0Zubg&=Z*gcXuyt`S5|8WB9f@G9r|p$l4x*{08GcO>7t z(Jg{8fT}v9`(OK`dcRuXM{T-Ivq~QbB2Vr??o-rXRNlZEB8L*vfmkb#-(ZUh0x03NVYK~W8_1>qeC6Hy{XK5CAATni^Qo!)(222�_C0yO-;i%w^Aq}bI8JJ= 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 literal 0 HcmV?d00001 diff --git a/Resources/images/LineCharts/RangedLineChart.png b/Resources/images/LineCharts/RangedLineChart.png new file mode 100644 index 0000000000000000000000000000000000000000..9c9215af9ea489fd4525f99e17c850dc2044a297 GIT binary patch literal 91338 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 *ca_8BYX-E6UU|9k|>5=fcG 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Ð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 z zqWKleq{&g9Nb&%#nnSvR4A!RVzgWHrMbVdQ;nt75&v=#ck02M{Ugq~3I^dd96!kll zg^Bg7)9BrH6J+FX)JPwBH7uPJmy}^F7JjY=vr2N#ZY0_TeLc+NG9VVWCW&@)%bg+! zD#k%I^0#RLv$<*IeOn)t#l*zE9HTQbgVKxdjvgp(Xza7Kfi30`W2@6cm{|F{bKn!J z6H3Nns(wpDOno}=sr!C^x(_()vEl`sux+e9SqhGgkRX%^&Ec|WaJQjmI9yH&1aK8eJJb* z^X<6yr!5C!rOaKpTY3W>n+Gzk(Ag$A8Fop`Ulbxr8xSsV;~wZ+att)HpvE zIFi#qp9<{zVgTiqyW zY60bwsCD7;VzzL^{1};6Za%W}wWg`Y*OyZ>VTjOl=&TcI%_uwX$SZa!Tts3fJ0lP3brbvw;#_zzKzlX$i;8TC90pOc1_l~pH)70`Jp8}#;+-D3f4 zj|NVo1D=MlT+>`tLPlSyOr3EFqpC)TCRf`|+Io=is&xu2C}G62alqiZ zL52~5q~_KaDraUo;7vw@Dbv%_O`Jfy**nH~egy}DD&*8Z*+#Ee}RVT!-{m6{|JlQ8+#Q%UL)uKT__G_W75;(DT^FN4DG0%$en0P@hJFC<=J00f3l(b(%RzrghUSI&FAa z_Qo(8X;2{!VZ;U z8g<8_=i#~KEViyx4;X6IplHT;A3>40!aT-!AyupOCb8KtQ9Z%QI`Z36EZGJfVCaCP z@EkPZI)T!0n37(W`iNGzF*?LI;v3Yw4@tF<5EKtu%YCO2a~p6iHv_o^A@tB37=i`6^dNHgvgza>=YDb zye)E_%(w!>(t|UZTtcyr&-?qVCazR<`Z_pwoV0fHR^}`}5G~(cLXNR|3&Fc=L2h!t z%U|H>k#C{39ZP`3vBaCJu?{+URE+IoVy#mU6dcX!R@qy2?rNWgHBFLUI`BUPgtq9y*OK>|euBu6IlN@61;Y{tya_kn)5a~(f>O`JvAkS`(*)y)asrED3P2q?rs=t98kn_w%H=oo?BKKJt0^SZorp>5?v+N`uO zbUK3vL*#6kl`@md$`->|YFzxdPCQ!7*y5$+U=Jpj!D2=39|h}S4}!%qn<5?@i~qq4 z2yg*A4&u-+i9b+3R~YcYAIGXed3t%J`iR6AO7)p{liWKYn=e~{&M2j z9~JRh0%qWmL3;OI4}*R5`BR7-AV}=d-TRo}J1_uFI>SQvccKH#U=K)>>ePb2VcFj3 z5CdS35q@?}^Pi78z+454FZZ?WkN#_5G@vN-B|il1gBV5{eh)NKgWjzIhQz(chartData: T, diff --git a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift index b1b8d003..f795720f 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/ViewModifiers/YAxisPOI.swift @@ -193,7 +193,7 @@ extension View { - 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`. + - labelColour: Colour of the `Text`. - labelBackground: Colour of the background. - lineColour: Line Colour. - strokeStyle: Style of Stroke. @@ -222,7 +222,7 @@ extension View { /** - Horizontal line marking the average + Horizontal line marking the average. Shows a marker line at the average of all the data points within the relevant data set(s). @@ -264,7 +264,7 @@ extension View { - 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`. + - labelColour: Colour of the `Text`. - labelBackground: Colour of the background. - lineColour: Line Colour. - strokeStyle: Style of Stroke. From ee40cad231c83776dcd0143f1fed5aa1df0fbb1d Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 16:40:47 +0000 Subject: [PATCH 144/152] Update ReadMe. --- README.md | 452 +++++++++++++++++++----------------------------------- 1 file changed, 155 insertions(+), 297 deletions(-) diff --git a/README.md b/README.md index c1d85a2d..03b8fa23 100644 --- a/README.md +++ b/README.md @@ -52,34 +52,64 @@ Swift Package Manager ``` File > Swift Packages > Add Package Dependency... ``` +```swift +import SwiftUICharts +``` ## Documentation +## Chart Types + +### LineChart + +Uses `LineChartData` data model. + +``` +LineChart(chartData: data) + .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())]) +``` + +--- + + +## View Modifiers + [View Modifiers](#View-Modifiers) -- [Touch Overlay](#Touch-Overlay) -- [Point Markers](#Point-Markers) +- [Touch Overlay](#Touch-Overlay) +- [Info Box](#Info-Box) +- [Floating Info Box](#Floating-Info-Box) +- [Header Box](#Header-Box) +- [Legends](#Legends) + - [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) +- [Point Markers](#Point-Markers) -## 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. ### All Chart Types @@ -87,7 +117,7 @@ File > Swift Packages > Add Package Dependency... Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information where specified. -The location of the info box is set in [ChartStyle](#ChartStyle) -> infoBoxPlacement. +The location of the info box is set in `ChartStyle -> infoBoxPlacement`. ```swift .touchOverlay(chartData: CTChartData, specifier: String, unit: TouchUnit) @@ -96,7 +126,7 @@ The location of the info box is set in [ChartStyle](#ChartStyle) -> infoBoxPlace - specifier: Decimal precision for labels. - unit: Unit to put before or after the value. -Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle) +Setup within Chart Data --> Chart Style --- @@ -104,9 +134,9 @@ Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle) #### Info Box -Displays the information from [Touch Overlay](#TouchOverlay) if `InfoBoxPlacement` is set to `.infoBox`. +Displays the information from [Touch Overlay](#Touch-Overlay) if `InfoBoxPlacement` is set to `.infoBox`. -The location of the info box is set in [ChartStyle](#ChartStyle) -> infoBoxPlacement. +The location of the info box is set in `ChartStyle -> infoBoxPlacement`. ```swift .infoBox(chartData: CTChartData) @@ -119,9 +149,9 @@ The location of the info box is set in [ChartStyle](#ChartStyle) -> infoBoxPlace #### Floating Info Box -Displays the information from [Touch Overlay](#TouchOverlay) if `InfoBoxPlacement` is set to `.floating`. +Displays the information from [Touch Overlay](#Touch-Overlay) if `InfoBoxPlacement` is set to `.floating`. -The location of the info box is set in [ChartStyle](#ChartStyle) -> infoBoxPlacement. +The location of the info box is set in `ChartStyle -> infoBoxPlacement`. ```swift .floatingInfoBox(chartData: CTChartData) @@ -134,11 +164,11 @@ The location of the info box is set in [ChartStyle](#ChartStyle) -> infoBoxPlace #### Header Box -Displays the metadata about the chart. See [ChartMetadata](#ChartMetadata). +Displays the metadata about the chart, set in `Chart Data -> ChartMetadata` -Displays the information from [Touch Overlay](#TouchOverlay) if `InfoBoxPlacement` is set to `.header`. +Displays the information from [Touch Overlay](#Touch-Overlay) if `InfoBoxPlacement` is set to `.header`. -The location of the info box is set in [ChartStyle](#ChartStyle) -> infoBoxPlacement. +The location of the info box is set in `ChartStyle -> infoBoxPlacement`. ```swift .headerBox(chartData: data) @@ -150,7 +180,7 @@ The location of the info box is set in [ChartStyle](#ChartStyle) -> infoBoxPlace #### 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)). +Displays legends. ```swift .legends() @@ -221,7 +251,7 @@ Adds vertical lines along the X axis. ```swift .xAxisGrid(chartData: CTLineBarChartDataProtocol) ``` -Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle). +Setup within `ChartData -> ChartStyle`. --- @@ -234,7 +264,7 @@ Adds horizontal lines along the Y axis. ```swift .yAxisGrid(chartData: CTLineBarChartDataProtocol) ``` -Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle). +Setup within `ChartData -> ChartStyle`. --- @@ -247,7 +277,7 @@ Labels for the X axis. ```swift .xAxisLabels(chartData: CTLineBarChartDataProtocol) ``` -Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle). +Setup within `ChartData -> ChartStyle`. --- @@ -262,7 +292,7 @@ Automatically generated labels for the Y axis ``` - specifier: Decimal precision specifier. -Setup within [ChartData](#ChartData) --> [ChartStyle](#ChartStyle). +Setup within `ChartData -> ChartStyle`. --- @@ -277,277 +307,105 @@ Lays out markers over each of the data point. ```swift .pointMarkers(chartData: CTLineChartDataProtocol) ``` -Setup within [ChartData](#DataSet) --> [PointStyle](#PointStyle). - - - - - -## Data Models - -### ChartData - -The ChartData type is where the majority of the configuration is done. The only required initialiser is dataPoints. - -```swift -ChartData(dataPoints : [ChartDataPoint], - metadata : ChartMetadata, - xAxisLabels : [String]?, - chartStyle : ChartStyle, - lineStyle : LineStyle, - barStyle : BarStyle, - pointStyle : PointStyle, - calculations : CalculationType) -``` -- 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. - - -### ChartDataPoint - -ChartDataPoint holds the information for each of the individual data points. - -Colours are only used in Bar Charts. - -__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. - -__Single Colour__ -```swift -ChartDataPoint(... - colour: Color) - -``` -- colour: Colour for use with a bar chart. - -__Colour Gradient__ -```swift -ChartDataPoint(... - colours : [Color]?, - startPoint : UnitPoint?, - endPoint : UnitPoint?) -``` -- colours: Colours for Gradient -- startPoint: Start point for Gradient -- endPoint: End point for Gradient - -__Colour Gradient with stop control__ -```swift -ChartDataPoint(... - 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. - - -### ChartMetadata - -Data model for the chart's metadata - -```swift -ChartMetadata(title: String?, - subtitle: String?, - lineLegend: String?) -``` -- title: The charts Title -- subtitle: The charts subtitle -- lineLegend: The title for the legend +Setup within `Data Set -> PointStyle`. -### ChartStyle - -Model for controlling the overall aesthetic of the chart. - -```swift -ChartStyle(infoBoxPlacement : InfoBoxPlacement, - xAxisGridStyle : GridStyle, - yAxisGridStyle : GridStyle, - xAxisLabelPosition : XAxisLabelPosistion, - xAxisLabelsFrom : LabelsFrom, - yAxisLabelPosition : YAxisLabelPosistion, - yAxisNumberOfLabels : Int, - globalAnimation : Animation -``` -- 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: Global control of animations. - - -### GridStyle - -Model for controlling the look of the Grid - -```swift -GridStyle(numberOfLines : Int, - lineColour : Color, - lineWidth : CGFloat, - dash : [CGFloat], - dashPhase : CGFloat) -``` -- numberOfLines: Number of lines to break up the axis -- lineColour: Line Colour -- lineWidth: Line Width -- dash: Dash -- dashPhase: Dash Phase - - -### XAxisLabelSetup - -Model for the styling of the labels on the X axis. - -```swift -XAxisLabelSetup(labelPosition: XAxisLabelPosistion, - labelsFrom: LabelsFrom) -``` -- labelPosition: Location of the X axis labels - Top or Bottom -- labelsFrom: Where the label data come from. DataPoint or xAxisLabels - - -### YAxisLabelSetup - -Model for the styling of the labels on the Y axis. - -```swift -YAxisLabelSetup(labelPosition : YAxisLabelPosistion, - numberOfLabels : Int) -``` -- labelPosition: Location of the Y axis labels - Leading or Trailing -- numberOfLabels: Number Of Labels on Y Axis - - -### 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. - -__Single Colour__ -```swift -LineChartStyle(colour: Color, - ... -``` -- colour: Single Colour - -__Colour Gradient__ -```swift -LineChartStyle(colours: [Color]?, - startPoint: UnitPoint?, - endPoint: UnitPoint?, - ... -``` -- colours: Colours for Gradient -- startPoint: Start point for Gradient -- endPoint: End point for Gradient - -__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__ -```swift -LineChartStyle(... - strokeStyle : StrokeStyle, - ignoreZero: Bool) -``` -- 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. - - -### 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. +--- -__Single Colour__ -```swift -BarStyle(... - colour: Single Colour) -``` -- colour: Single Colour +## Examples -__Colour Gradient__ -```swift -BarStyle(... - colours : [Color] - startPoint : UnitPoint - endPoint : UnitPoint) +### Line Chart + +```Swift +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())]) + .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) + + } +} ``` -- colours: Colours for Gradient -- startPoint: Start point for Gradient -- endPoint: End point for Gradient -__Colour Gradient with stop control__ -```swift -BarStyle(... - 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. - -### PointStyle - -Model for controlling the aesthetic of the point markers. - -```swift -PointStyle(pointSize : CGFloat, - borderColour : Color, - fillColour : Color, - lineWidth : CGFloat, - pointType : PointType, - pointShape : PointShape) -``` -- 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 +--- From cd283436e4b1e54691b3074586a34d2e39864721 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 16:54:55 +0000 Subject: [PATCH 145/152] Update docs. --- README.md | 143 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 117 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 03b8fa23..41efb30e 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ A charts / plotting library for SwiftUI. Works on macOS, iOS, watchOS, and tvOS ![Example of Line Chart](Resources/images/PieCharts/DoughnutChart.png) -## Installation +## Documentation +### Installation Swift Package Manager @@ -56,38 +57,128 @@ File > Swift Packages > Add Package Dependency... import SwiftUICharts ``` -## Documentation -## Chart Types +--- + + +### Chart Types + +#### Line Chart + +Uses `LineChartData` data model. + +```swift +LineChart(chartData: LineChartData) +``` + + +--- + -### LineChart +#### Filled Line Chart Uses `LineChartData` data model. +```swift +FilledLineChart(chartData: LineChartData) +``` + + +--- + + +#### Multi Line Chart + +Uses `MultiLineChartData` data model. + +```swift +MultiLineChart(chartData: MultiLineChartData) +``` + + +--- + + +#### Ranged Line Chart + +Uses `RangedLineChart` data model. + +```swift +RangedLineChart(chartData: RangedLineChartData) +``` + + +--- + + +#### Bar Chart + +Uses `BarChartData` data model. + +```swift +BarChart(chartData: BarChartData) +``` + + +--- + + +#### Ranged Bar Chart + +Uses `RangedBarChartData` data model. + +```swift +RangedBarChart(chartData: RangedBarChartData) +``` + + +--- + + +#### Grouped Bar Chart + +Uses `GroupedBarChartData` data model. + +```swift +GroupedBarChart(chartData: GroupedBarChartData) +``` + + +--- + + +#### Stacked Bar Chart + +Uses `StackedBarChartData` data model. + +```swift +StackedBarChart(chartData: StackedBarChartData) +``` + +--- + + +#### Pie Chart + +Uses `PieChartData` data model. + +```swift +PieChart(chartData: PieChartData) ``` -LineChart(chartData: data) - .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())]) + + +--- + + +#### Doughnut Chart + +Uses `DoughnutChartData` data model. + +```swift +DoughnutChart(chartData: DoughnutChartData) ``` + --- @@ -109,7 +200,7 @@ LineChart(chartData: data) - [Point Markers](#Point-Markers) -The order of the view modifiers is some what important as the modifiers are various types for stacks that wrap around the previous views. +The order of the view modifiers is some what important as the modifiers are various types of stacks that wrap around the previous views. ### All Chart Types From 79d975069fb304f9edbd9a5bad653d0eaa13fde3 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Fri, 12 Mar 2021 17:10:54 +0000 Subject: [PATCH 146/152] Update readme. --- README.md | 16 ++++++++++++++-- Resources/images/PieCharts/DoughnutChart.png | Bin 103186 -> 83115 bytes Resources/images/PieCharts/PieChart.png | Bin 76067 -> 59601 bytes 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 41efb30e..460e6222 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,19 @@ import SwiftUICharts ### Chart Types +- [Line Chart](#Line-Chart) +- [Filled Line Chart](#Filled-Line-Chart) +- [Multi Line Chart](#Multi-Line-Chart) +- [Ranged Line Chart](#Ranged-Line-Chart) + +- [Bar Chart](#Bar-Chart) +- [Ranged Bar Chart](#Ranged-Bar-Chart) +- [Grouped Bar Chart](#Grouped-Bar-Chart) +- [Stacked Bar Chart](#Stacked-Bar-Chart) + +- [Pie Chart](#Pie-Chart) +- [Doughnut Chart](#Doughnut-Chart) + #### Line Chart Uses `LineChartData` data model. @@ -184,7 +197,6 @@ DoughnutChart(chartData: DoughnutChartData) ## View Modifiers -[View Modifiers](#View-Modifiers) - [Touch Overlay](#Touch-Overlay) - [Info Box](#Info-Box) - [Floating Info Box](#Floating-Info-Box) @@ -407,7 +419,7 @@ Setup within `Data Set -> PointStyle`. ### Line Chart -```Swift +```swift struct LineChartDemoView: View { let data : LineChartData = weekOfData() diff --git a/Resources/images/PieCharts/DoughnutChart.png b/Resources/images/PieCharts/DoughnutChart.png index bfbe4abfdd062912338450c8e3f3ef59961a1682..bb0fd14e3e7c4bd48920edc8149e8eab5e0f2cb7 100644 GIT binary patch literal 83115 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Ð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

{j?|||DZFAv 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 z zqWKleq{&g9Nb&%#nnSvR4A!RVzgWHrMbVdQ;nt75&v=#ck02M{Ugq~3I^dd96!kll zg^Bg7)9BrH6J+FX)JPwBH7uPJmy}^F7JjY=vr2N#ZY0_TeLc+NG9VVWCW&@)%bg+! zD#k%I^0#RLv$<*IeOn)t#l*zE9HTQbgVKxdjvgp(Xza7Kfi30`W2@6cm{|F{bKn!J z6H3Nns(wpDOno}=sr!C^x(_()vEl`sux+e9SqhGgkRX%^&Ec|WaJQjmI9yH&1aK8eJJb* z^X<6yr!5C!rOaKpTY3W>n+Gzk(Ag$A8Fop`Ulbxr8xSsV;~wZ+att)HpvE zIFi#qp9<{zVgTiqyW zY60bwsCD7;VzzL^{1};6Za%W}wWg`Y*OyZ>VTjOl=&TcI%_uwX$SZa!Tts3fJ0lP3brbvw;#_zzKzlX$i;8TC90pOc1_l~pH)70`Jp8}#;+-D3f4 zj|NVo1D=MlT+>`tLPlSyOr3EFqpC)TCRf`|+Io=is&xu2C}G62alqiZ zL52~5q~_KaDraUo;7vw@Dbv%_O`Jfy**nH~egy}DD&*8Z*+#Ee}RVT!-{m6{|JlQ8+#Q%UL)uKT__G_W75;(DT^FN4DG0%$en0P@hJFC<=J00f3l(b(%RzrghUSI&FAa z_Qo(8X;2{!VZ;U z8g<8_=i#~KEViyx4;X6IplHT;A3>40!aT-!AyupOCb8KtQ9Z%QI`Z36EZGJfVCaCP z@EkPZI)T!0n37(W`iNGzF*?LI;v3Yw4@tF<5EKtu%YCO2a~p6iHv_o^A@tB37=i`6^dNHgvgza>=YDb zye)E_%(w!>(t|UZTtcyr&-?qVCazR<`Z_pwoV0fHR^}`}5G~(cLXNR|3&Fc=L2h!t z%U|H>k#C{39ZP`3vBaCJu?{+URE+IoVy#mU6dcX!R@qy2?rNWgHBFLUI`BUPgtq9y*OK>|euBu6IlN@61;Y{tya_kn)5a~(f>O`JvAkS`(*)y)asrED3P2q?rs=t98kn_w%H=oo?BKKJt0^SZorp>5?v+N`uO zbUK3vL*#6kl`@md$`->|YFzxdPCQ!7*y5$+U=Jpj!D2=39|h}S4}!%qn<5?@i~qq4 z2yg*A4&u-+i9b+3R~YcYAIGXed3t%J`iR6AO7)p{liWKYn=e~{&M2j z9~JRh0%qWmL3;OI4}*R5`BR7-AV}=d-TRo}J1_uFI>SQvccKH#U=K)>>ePb2VcFj3 z5CdS35q@?}^Pi78z+454FZZ?WkN#_5G@vN-B|il1gBV5{eh)NKgWjzIhQz Date: Sat, 13 Mar 2021 10:16:18 +0000 Subject: [PATCH 147/152] Update --- README.md | 98 +++++++++++++++++++++---------------------------------- 1 file changed, 37 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 460e6222..1dd8fe3a 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,11 @@ # SwiftUICharts -A charts / plotting library for SwiftUI. Works on macOS, iOS, watchOS, and tvOS. Has accessibility features built in +A charts / plotting library for SwiftUI. Works on macOS, iOS, watchOS, and tvOS. Has accessibility features built in. [Demo Project](https://github.com/willdale/SwiftUICharts-Demo) -## Examples - -### Line Charts - -#### Line Chart -![Example of Line Chart](Resources/images/LineCharts/LineChart.png) -#### Filled Line Chart -![Example of Line Chart](Resources/images/LineCharts/FilledLineChart.png) - -#### Multi Line Chart -![Example of Line Chart](Resources/images/LineCharts/MultiLineChart.png) - -#### Ranged Line Chart -![Example of Line Chart](Resources/images/LineCharts/RangedLineChart.png) - - -### Bar Charts - -#### Bar Chart -![Example of Line Chart](Resources/images/BarCharts/BarChart.png) - -#### Range Bar Chart -![Example of Line Chart](Resources/images/BarCharts/RangeBarChart.png) - -#### Grouped Bar Chart -![Example of Line Chart](Resources/images/BarCharts/GroupedBarChart.png) - -#### Stacked Bar Chart -![Example of Line Chart](Resources/images/BarCharts/StackedBarChart.png) - - -### Pie Charts - -#### Pie Chart -![Example of Line Chart](Resources/images/PieCharts/PieChart.png) - -#### Doughnut Chart -![Example of Line Chart](Resources/images/PieCharts/DoughnutChart.png) - - -## Documentation -### Installation - -Swift Package Manager - -``` -File > Swift Packages > Add Package Dependency... -``` -```swift -import SwiftUICharts -``` - - ---- - - -### Chart Types +## Chart Types - [Line Chart](#Line-Chart) - [Filled Line Chart](#Filled-Line-Chart) @@ -76,7 +20,11 @@ import SwiftUICharts - [Pie Chart](#Pie-Chart) - [Doughnut Chart](#Doughnut-Chart) + +### Line Charts + #### Line Chart +![Example of Line Chart](Resources/images/LineCharts/LineChart.png) Uses `LineChartData` data model. @@ -84,11 +32,11 @@ Uses `LineChartData` data model. LineChart(chartData: LineChartData) ``` - --- #### Filled Line Chart +![Example of Filled Line Chart](Resources/images/LineCharts/FilledLineChart.png) Uses `LineChartData` data model. @@ -101,6 +49,7 @@ FilledLineChart(chartData: LineChartData) #### Multi Line Chart +![Example of Multi Line Chart](Resources/images/LineCharts/MultiLineChart.png) Uses `MultiLineChartData` data model. @@ -113,6 +62,7 @@ MultiLineChart(chartData: MultiLineChartData) #### Ranged Line Chart +![Example of Ranged Line Chart](Resources/images/LineCharts/RangedLineChart.png) Uses `RangedLineChart` data model. @@ -124,7 +74,10 @@ RangedLineChart(chartData: RangedLineChartData) --- +### Bar Charts + #### Bar Chart +![Example of Bar Chart](Resources/images/BarCharts/BarChart.png) Uses `BarChartData` data model. @@ -136,7 +89,8 @@ BarChart(chartData: BarChartData) --- -#### Ranged Bar Chart +#### Range Bar Chart +![Example of Range Bar Chart](Resources/images/BarCharts/RangeBarChart.png) Uses `RangedBarChartData` data model. @@ -148,7 +102,9 @@ RangedBarChart(chartData: RangedBarChartData) --- + #### Grouped Bar Chart +![Example of Grouped Bar Chart](Resources/images/BarCharts/GroupedBarChart.png) Uses `GroupedBarChartData` data model. @@ -161,6 +117,7 @@ GroupedBarChart(chartData: GroupedBarChartData) #### Stacked Bar Chart +![Example of Stacked Bar Chart](Resources/images/BarCharts/StackedBarChart.png) Uses `StackedBarChartData` data model. @@ -171,7 +128,10 @@ StackedBarChart(chartData: StackedBarChartData) --- +### Pie Charts + #### Pie Chart +![Example of Pie Chart](Resources/images/PieCharts/PieChart.png) Uses `PieChartData` data model. @@ -184,6 +144,7 @@ PieChart(chartData: PieChartData) #### Doughnut Chart +![Example of Doughnut Chart](Resources/images/PieCharts/DoughnutChart.png) Uses `DoughnutChartData` data model. @@ -192,6 +153,21 @@ DoughnutChart(chartData: DoughnutChartData) ``` +--- + +## Documentation +### Installation + +Swift Package Manager + +``` +File > Swift Packages > Add Package Dependency... +``` +```swift +import SwiftUICharts +``` + + --- @@ -274,7 +250,7 @@ Displays the information from [Touch Overlay](#Touch-Overlay) if `InfoBoxPlaceme The location of the info box is set in `ChartStyle -> infoBoxPlacement`. ```swift -.headerBox(chartData: data) +.headerBox(chartData: CTChartData) ``` From 7950bc63710b789d62be991533bec0c7ab659c78 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 14 Mar 2021 10:10:04 +0000 Subject: [PATCH 148/152] Add Ignore Zero back in. --- .../LineChart/Extras/PathExtensions.swift | 151 ++++++++++++++---- .../Models/ChartData/LineChartData.swift | 28 +++- .../Models/ChartData/MultiLineChartData.swift | 34 +++- .../ChartData/RangedLineChartData.swift | 55 +++---- .../Models/Protocols/LineChartProtocols.swift | 2 + .../LineChartProtocolsExtensions.swift | 59 ++++--- .../LineChart/Shapes/LineShape.swift | 20 ++- .../LineChart/Shapes/PointShape.swift | 43 +++-- .../LineChart/Views/FilledLineChart.swift | 1 + .../LineChart/Views/RangedLineChart.swift | 29 ++-- .../Views/SubViews/LineChartSubViews.swift | 21 +-- 11 files changed, 305 insertions(+), 138 deletions(-) diff --git a/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift b/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift index b075fac3..54eb721d 100644 --- a/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Extras/PathExtensions.swift @@ -7,20 +7,36 @@ import SwiftUI -// MARK: - Paths extension Path { /// Draws straight lines between data points. - static func straightLine(rect: CGRect, dataPoints: [DP], minValue: Double, range: Double, isFilled: Bool) -> Path { + 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) - path.addLine(to: nextPoint) + + 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)) @@ -31,7 +47,14 @@ extension Path { } /// Draws cubic BĆ©zier curved lines between data points. - static func curvedLine(rect: CGRect, dataPoints: [DP], minValue: Double, range: Double, isFilled: Bool) -> Path { + 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() @@ -39,21 +62,36 @@ extension Path { 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) - 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)) + 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 - path.addLine(to: CGPoint(x: CGFloat(dataPoints.count-1) * x, + // 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)) @@ -65,7 +103,13 @@ extension Path { /// Draws straight lines between data points. - static func straightLineBox(rect: CGRect, dataPoints: [DP], minValue: Double, range: Double) -> Path { + 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) @@ -73,31 +117,52 @@ extension Path { // Upper Path let firstPointUpper = CGPoint(x: 0, - y: (CGFloat(dataPoints[0].upperValue - minValue) * -y) + rect.height) + 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) - path.addLine(to: nextPointUpper) + 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) - path.addLine(to: nextPointLower) + 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) -> Path { + 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 @@ -111,11 +176,22 @@ extension Path { let nextPoint = CGPoint(x: CGFloat(indexUpper) * x, y: (CGFloat(dataPoints[indexUpper].upperValue - minValue) * -y) + rect.height) - 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)) + + 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 } @@ -123,11 +199,22 @@ extension Path { for indexLower in (0 ..< dataPoints.count).reversed() { let nextPoint = CGPoint(x: CGFloat(indexLower) * x, y: (CGFloat(dataPoints[indexLower].lowerValue - minValue) * -y) + rect.height) - 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)) + + 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 } diff --git a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift index fbcb41f4..2d10fd74 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/LineChartData.swift @@ -131,21 +131,37 @@ extension LineChartData { let index : Int = Int((touchLocation.x + (xSection / 2)) / xSection) if index >= 0 && index < dataSet.dataPoints.count { - return CGPoint(x: CGFloat(index) * xSection, - y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.height) + + 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 { - var dataPoint = dataSets.dataPoints[index] - dataPoint.legendTag = dataSets.legendTitle - points.append(dataPoint) + 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 index 9b3e45ba..911337ea 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/MultiLineChartData.swift @@ -130,9 +130,9 @@ public final class MultiLineChartData: CTLineChartDataProtocol { } } - public typealias Set = MultiLineDataSet - public typealias DataPoint = LineChartDataPoint - public typealias CTStyle = LineChartStyle + public typealias Set = MultiLineDataSet + public typealias DataPoint = LineChartDataPoint + public typealias CTStyle = LineChartStyle } @@ -148,20 +148,38 @@ extension MultiLineChartData { let index : Int = Int((touchLocation.x + (xSection / 2)) / xSection) if index >= 0 && index < dataSet.dataPoints.count { - return CGPoint(x: CGFloat(index) * xSection, - y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.height) + + 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 { - var dataPoint = dataSet.dataPoints[index] - dataPoint.legendTag = dataSet.legendTitle - points.append(dataPoint) + 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 index 7a020f0f..160dc1bf 100644 --- a/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift +++ b/Sources/SwiftUICharts/LineChart/Models/ChartData/RangedLineChartData.swift @@ -128,51 +128,38 @@ public final class RangedLineChartData: CTLineChartDataProtocol { let index : Int = Int((touchLocation.x + (xSection / 2)) / xSection) if index >= 0 && index < dataSet.dataPoints.count { - return CGPoint(x: CGFloat(index) * xSection, - y: (CGFloat(dataSet.dataPoints[index].value - minValue) * -ySection) + chartSize.height) + 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 { - var dataPoint = dataSets.dataPoints[index] - dataPoint.legendTag = dataSets.legendTitle - points.append(dataPoint) - } - self.infoView.touchOverlayInfo = points - } - - public func headerTouchOverlaySubView(info: RangedLineChartDataPoint) -> some View { - Group { - switch self.infoView.touchUnit { - case .none: - Text("\(info.upperValue, specifier: self.infoView.touchSpecifier)") - .font(.title3) - .foregroundColor(self.chartStyle.infoBoxValueColour) - Text("\(info.wrappedDescription)") - .font(.subheadline) - .foregroundColor(self.chartStyle.infoBoxDescriptionColour) - case .prefix(of: let unit): - Text("\(unit) \(info.upperValue, specifier: self.infoView.touchSpecifier)") - .font(.title3) - .foregroundColor(self.chartStyle.infoBoxValueColour) - Text("\(info.wrappedDescription)") - .font(.subheadline) - .foregroundColor(self.chartStyle.infoBoxDescriptionColour) - case .suffix(of: let unit): - Text("\(info.upperValue, specifier: self.infoView.touchSpecifier) \(unit)") - .font(.title3) - .foregroundColor(self.chartStyle.infoBoxValueColour) - Text("\(info.wrappedDescription)") - .font(.subheadline) - .foregroundColor(self.chartStyle.infoBoxDescriptionColour) + + 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 diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift index 5210f665..6b8283b2 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocols.swift @@ -55,6 +55,8 @@ public protocol CTLineStyle { Replica of Appleā€™s StrokeStyle that conforms to Hashable */ var strokeStyle : Stroke { get set } + + var ignoreZero : Bool { get set } } /** diff --git a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift index 37cc8411..08a86905 100644 --- a/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift +++ b/Sources/SwiftUICharts/LineChart/Models/Protocols/LineChartProtocolsExtensions.swift @@ -12,12 +12,14 @@ 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 + 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, @@ -25,8 +27,8 @@ extension CTLineChartDataProtocol { dataPoints : dataPoints, minValue : minValue, range : range, - touchLocation: touchLocation, - isFilled : false) + isFilled : false, + ignoreZero : ignoreZero) return Self.locationOnPath(Self.getPercentageOfPath(path: path, touchLocation: touchLocation), path) } } @@ -44,20 +46,30 @@ extension CTLineChartDataProtocol { - 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, touchLocation: CGPoint, isFilled: Bool) -> Path { + 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) + isFilled : isFilled, + ignoreZero : ignoreZero) case .curvedLine: return Path.curvedLine(rect : rect, dataPoints : dataPoints, minValue : minValue, range : range, - isFilled : isFilled) + isFilled : isFilled, + ignoreZero : ignoreZero) } } @@ -246,7 +258,7 @@ extension CTLineChartDataProtocol { // MARK: - Markers extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { - internal func markerSubView (dataSet : DS, dataPoints : [DP], @@ -268,7 +280,8 @@ extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { touchLocation: touchLocation, lineType: lineType, minValue: self.minValue, - range: self.range)) + range: self.range, + ignoreZero: dataSet.style.ignoreZero)) case .vertical(attachment: let attach): @@ -280,7 +293,8 @@ extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { touchLocation: touchLocation, lineType: lineType, minValue: self.minValue, - range: self.range) + range: self.range, + ignoreZero: dataSet.style.ignoreZero) Vertical(position: position) .stroke(Color.primary, lineWidth: 2) @@ -306,12 +320,14 @@ extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { touchLocation: touchLocation, lineType: lineType, minValue: self.minValue, - range: self.range) + range: self.range, + ignoreZero: dataSet.style.ignoreZero) MarkerFull(position: position) .stroke(Color.primary, lineWidth: 2) IndicatorSwitch(indicator: indicator, location: position) + case .point: @@ -334,7 +350,8 @@ extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { touchLocation: touchLocation, lineType: lineType, minValue: self.minValue, - range: self.range) + range: self.range, + ignoreZero: dataSet.style.ignoreZero) MarkerBottomLeading(position: position) .stroke(Color.primary, lineWidth: 2) @@ -362,7 +379,8 @@ extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { touchLocation: touchLocation, lineType: lineType, minValue: self.minValue, - range: self.range) + range: self.range, + ignoreZero: dataSet.style.ignoreZero) MarkerBottomTrailing(position: position) .stroke(Color.primary, lineWidth: 2) @@ -390,7 +408,8 @@ extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { touchLocation: touchLocation, lineType: lineType, minValue: self.minValue, - range: self.range) + range: self.range, + ignoreZero: dataSet.style.ignoreZero) MarkerTopLeading(position: position) .stroke(Color.primary, lineWidth: 2) @@ -418,7 +437,8 @@ extension CTLineChartDataProtocol where Self.CTStyle.Mark == LineMarkerType { touchLocation: touchLocation, lineType: lineType, minValue: self.minValue, - range: self.range) + range: self.range, + ignoreZero: dataSet.style.ignoreZero) MarkerTopTrailing(position: position) .stroke(Color.primary, lineWidth: 2) @@ -460,6 +480,7 @@ internal struct IndicatorSwitch: View { PosistionIndicator(fillColour: style.fillColour, lineColour: style.lineColour, lineWidth: style.lineWidth) .frame(width: style.size, height: style.size) .position(location) + } } diff --git a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift index 695dc4e4..75626541 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/LineShape.swift @@ -19,25 +19,29 @@ internal struct LineShape: Shape where DP: CTStandardDataPointProtocol { private let minValue : Double private let range : Double + private let ignoreZero: Bool + internal init(dataPoints: [DP], lineType : LineType, isFilled : Bool, minValue : Double, - range : Double + range : Double, + ignoreZero: Bool ) { self.dataPoints = dataPoints self.lineType = lineType self.isFilled = isFilled self.minValue = minValue self.range = range + self.ignoreZero = ignoreZero } internal func path(in rect: CGRect) -> Path { switch lineType { case .curvedLine: - return Path.curvedLine(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled) + return Path.curvedLine(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled, ignoreZero: ignoreZero) case .line: - return Path.straightLine(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled) + return Path.straightLine(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range, isFilled: isFilled, ignoreZero: ignoreZero) } } } @@ -54,24 +58,28 @@ internal struct RangedLineFillShape: Shape where DP: CTRangedLineDataPoint { private var minValue : Double private let range : Double + private let ignoreZero: Bool + internal init(dataPoints: [DP], lineType : LineType, minValue : Double, - range : 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: - return Path.curvedLineBox(rect: rect, dataPoints: dataPoints, minValue: minValue, range: range) + 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) + 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 index 850a7859..0027f8bc 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift @@ -17,6 +17,7 @@ internal struct Point: Shape where T: CTLineChartDataSet, private let minValue : Double private let range : Double + internal init(dataSet : T, minValue : Double, @@ -34,11 +35,17 @@ internal struct Point: Shape where T: CTLineChartDataSet, 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) - pointSwitch(&path, firstPoint) + 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) @@ -47,21 +54,33 @@ internal struct Point: Shape where T: CTLineChartDataSet, y : pointY, width : dataSet.pointStyle.pointSize, height: dataSet.pointStyle.pointSize) - pointSwitch(&path, point) + 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) - pointSwitch(&path, lastPoint) + 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. diff --git a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift index 65f8f099..32675140 100644 --- a/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/FilledLineChart.swift @@ -41,6 +41,7 @@ import SwiftUI .legends(chartData: data) ``` */ + public struct FilledLineChart: View where ChartData: LineChartData { @ObservedObject var chartData: ChartData diff --git a/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift b/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift index f59cfbb9..fd295e8e 100644 --- a/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift +++ b/Sources/SwiftUICharts/LineChart/Views/RangedLineChart.swift @@ -61,48 +61,53 @@ public struct RangedLineChart: View where ChartData: RangedLineChartD 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) + 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) + 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) + 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 diff --git a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift index 5d3fbe45..01c4bb9e 100644 --- a/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift +++ b/Sources/SwiftUICharts/LineChart/Views/SubViews/LineChartSubViews.swift @@ -69,7 +69,8 @@ internal struct LineChartColourSubView: View where CD: CTLineChartDataPr lineType : dataSet.style.lineType, isFilled : isFilled, minValue : minValue, - range : range) + range : range, + ignoreZero: dataSet.style.ignoreZero) .ifElse(isFilled, if: { $0.scale(y: startAnimation ? 1 : 0, anchor: .bottom) .fill(colour) @@ -139,10 +140,11 @@ internal struct LineChartColoursSubView: View where CD: CTLineChartDataP chartData.getAccessibility() LineShape(dataPoints: dataSet.dataPoints, - lineType: dataSet.style.lineType, - isFilled: isFilled, - minValue: minValue, - range: range) + 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) @@ -221,10 +223,11 @@ internal struct LineChartStopsSubView: View where CD: CTLineChartDataPro chartData.getAccessibility() LineShape(dataPoints: dataSet.dataPoints, - lineType: dataSet.style.lineType, - isFilled: isFilled, - minValue: minValue, - range: range) + lineType : dataSet.style.lineType, + isFilled : isFilled, + minValue : minValue, + range : range, + ignoreZero: dataSet.style.ignoreZero) .ifElse(isFilled, if: { $0 From e8417ef59d032e3f526db7a03d8ebef715a22a60 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Sun, 14 Mar 2021 10:12:03 +0000 Subject: [PATCH 149/152] Tidy up. --- .../Shared/Shapes/TouchOverlayMarker.swift | 12 ++-- .../Protocols/LineAndBarProtocols.swift | 2 +- .../LineAndBarProtocolsExtentions.swift | 69 ++++++++++--------- 3 files changed, 44 insertions(+), 39 deletions(-) diff --git a/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift b/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift index 5ceb9628..67a7c0b6 100644 --- a/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift +++ b/Sources/SwiftUICharts/Shared/Shapes/TouchOverlayMarker.swift @@ -12,7 +12,7 @@ internal struct Vertical: Shape { private var position : CGPoint - @inlinable internal init(position : CGPoint) { + internal init(position : CGPoint) { self.position = position } @@ -32,7 +32,7 @@ internal struct MarkerFull: Shape { private var position : CGPoint - @inlinable internal init(position : CGPoint) { + internal init(position : CGPoint) { self.position = position } @@ -57,7 +57,7 @@ internal struct MarkerBottomLeading: Shape { private var position : CGPoint - @inlinable internal init(position : CGPoint) { + internal init(position : CGPoint) { self.position = position } @@ -82,7 +82,7 @@ internal struct MarkerBottomTrailing: Shape { private var position : CGPoint - @inlinable internal init(position : CGPoint) { + internal init(position : CGPoint) { self.position = position } @@ -107,7 +107,7 @@ internal struct MarkerTopLeading: Shape { private var position : CGPoint - @inlinable internal init(position : CGPoint) { + internal init(position : CGPoint) { self.position = position } @@ -132,7 +132,7 @@ internal struct MarkerTopTrailing: Shape { private var position : CGPoint - @inlinable internal init(position : CGPoint) { + internal init(position : CGPoint) { self.position = position } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift index 200693ea..b073057f 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocols.swift @@ -29,7 +29,7 @@ public protocol CTLineBarChartDataProtocol: CTChartData where CTStyle: CTLineBar /** Returns the highest value in the data set or data sets */ - var maxValue: Double { get } + var maxValue: Double { get } /** Returns the average value from the data set or data sets. diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift index aa049290..f71e8ad7 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Models/Protocols/LineAndBarProtocolsExtentions.swift @@ -10,46 +10,51 @@ import SwiftUI // MARK: - Data Set extension CTLineBarChartDataProtocol { public var range : Double { - - 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 + 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 } - - 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 { - 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 + 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 { - switch self.chartStyle.topLine { - case .maximumValue: - return self.dataSets.maxValue() - case .maximum(of: let value): - return max(self.dataSets.maxValue(), value) + get { + switch self.chartStyle.topLine { + case .maximumValue: + return self.dataSets.maxValue() + case .maximum(of: let value): + return max(self.dataSets.maxValue(), value) + } } } From b5b3ce84468e49cb40cbeef6cc77c20508f5deab Mon Sep 17 00:00:00 2001 From: Will Dale Date: Mon, 15 Mar 2021 08:52:04 +0000 Subject: [PATCH 150/152] Tidy Up. --- Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift | 2 +- Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift b/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift index 0027f8bc..bfde15e2 100644 --- a/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift +++ b/Sources/SwiftUICharts/LineChart/Shapes/PointShape.swift @@ -65,7 +65,7 @@ internal struct Point: Shape where T: CTLineChartDataSet, 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 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, diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 71499a10..c4f5f5e4 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -96,7 +96,7 @@ extension View { */ public func touchOverlay(chartData: T, specifier: String = "%.0f", - unit : TouchUnit + unit : TouchUnit = .none ) -> some View { self.modifier(EmptyModifier()) } From 4de3bcd20e6042f2af694e22b3fff7bf451ce021 Mon Sep 17 00:00:00 2001 From: Will Dale Date: Mon, 15 Mar 2021 08:58:20 +0000 Subject: [PATCH 151/152] Update ReadMe. --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1dd8fe3a..08de1a87 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SwiftUICharts -A charts / plotting library for SwiftUI. Works on macOS, iOS, watchOS, and tvOS. Has accessibility features built in. +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) @@ -167,6 +167,11 @@ File > Swift Packages > Add Package Dependency... import SwiftUICharts ``` +If you have trouble with views not updating correctly, add `.id()` to your View. +```swift +LineChart(chartData: LineChartData) + .id(LineChartData.id) +``` --- @@ -190,6 +195,10 @@ import SwiftUICharts The order of the view modifiers is some what important as the modifiers are various types of stacks that wrap around the previous views. + +--- + + ### All Chart Types #### Touch Overlay @@ -430,6 +439,7 @@ struct LineChartDemoView: View { .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") From 57eee35dee0a3e875a33eef8192eef0d860515fb Mon Sep 17 00:00:00 2001 From: Will Dale <63838770+willdale@users.noreply.github.com> Date: Mon, 15 Mar 2021 10:07:36 +0000 Subject: [PATCH 152/152] Tidy up --- .../Shared/Extras/Extensions.swift | 22 -- .../Shared/Models/ChartMetadata.swift | 9 - .../Shared/ViewModifiers/HeaderBox.swift | 30 --- .../Shared/ViewModifiers/TouchOverlay.swift | 207 ------------------ .../Shared/Views/TouchOverlayBox.swift | 56 ----- .../SharedLineAndBar/Shapes/Marker.swift | 33 --- 6 files changed, 357 deletions(-) diff --git a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift index eca00d1a..3bb8b651 100644 --- a/Sources/SwiftUICharts/Shared/Extras/Extensions.swift +++ b/Sources/SwiftUICharts/Shared/Extras/Extensions.swift @@ -35,20 +35,6 @@ extension View { } extension View { -<<<<<<< HEAD - @ViewBuilder - func `ifElseElseIf`(_ condition: Bool, - _ secondCondition: Bool, - if ifTransform: (Self) -> TrueContent, - elseIf elseIfTransform: (Self) -> MidContent, - else elseTransform: (Self) -> FalseContent - ) -> some View { - - if condition { - ifTransform(self) - } else if secondCondition { - elseIfTransform(self) -======= /** View modifier to conditionally add a view modifier else add a different one. @@ -60,7 +46,6 @@ extension View { ) -> some View { if condition { ifTransform(self) ->>>>>>> version-2 } else { elseTransform(self) } @@ -83,24 +68,18 @@ 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 { @@ -116,5 +95,4 @@ extension Color { return Color(.windowBackgroundColor) #endif } ->>>>>>> version-2 } diff --git a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift index ee671b3f..0a1949cd 100644 --- a/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift +++ b/Sources/SwiftUICharts/Shared/Models/ChartMetadata.swift @@ -13,14 +13,6 @@ import SwiftUI 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 @@ -29,7 +21,6 @@ public struct ChartMetadata { public var titleColour : Color /// Color of the subtitle public var subtitleColour: Color ->>>>>>> version-2 /// Model to hold the metadata for the chart. /// - Parameters: diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift index 27ab78c6..1b8fc1eb 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/HeaderBox.swift @@ -32,35 +32,6 @@ internal struct HeaderBox: ViewModifier where T: CTChartData { var touchOverlay: some View { VStack(alignment: .trailing) { -<<<<<<< HEAD - if chartData.viewData.isTouchCurrent, - let value = chartData.viewData.touchOverlayInfo?.value { - - - switch chartData.viewData.units { - case .none: - Text("\(value, specifier: chartData.viewData.touchSpecifier)") - .font(.title3) - case .prefix(of: let units): - Text("\(units) \(value, specifier: chartData.viewData.touchSpecifier)") - .font(.title3) - case .suffix(of: let units): - Text("\(value, specifier: chartData.viewData.touchSpecifier) \(units)") - .font(.title3) - } - - - - } else { - Text("") - .font(.title3) - } - if chartData.viewData.isTouchCurrent, - let label = chartData.viewData.touchOverlayInfo?.pointDescription { - Text("\(label)") - .font(.subheadline) - } else { -======= if chartData.infoView.isTouchCurrent { ForEach(chartData.infoView.touchOverlayInfo, id: \.id) { point in @@ -76,7 +47,6 @@ internal struct HeaderBox: ViewModifier where T: CTChartData { } else { Text("") .font(.title3) ->>>>>>> version-2 Text("") .font(.subheadline) } diff --git a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift index 7250fc7a..c4f5f5e4 100644 --- a/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift +++ b/Sources/SwiftUICharts/Shared/ViewModifiers/TouchOverlay.swift @@ -8,40 +8,6 @@ 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 { - - @EnvironmentObject var chartData: ChartData - - /// Decimal precision for labels - private let specifier : String - private var units : Units - private let touchMarkerLineWidth : CGFloat = 1 // API? - - /// Boolean that indicates whether touch is currently being detected - @State private var isTouchCurrent : Bool = false - /// Current location of the touch input - @State private var touchLocation : CGPoint = CGPoint(x: 0, y: 0) - /// The data point closest to the touch input - @State private var selectedPoint : ChartDataPoint? - /// The location for the nearest data point to the touch input - @State private var pointLocation : CGPoint = CGPoint(x: 0, y: 0) - /// Frame information of the data point information box - @State private var boxFrame : CGRect = CGRect(x: 0, y: 0, width: 0, height: 50) - /// Placement of the data point information box - @State private var boxLocation : CGPoint = CGPoint(x: 0, y: 0) - /// Placement of place the markers intersecting the data points location - @State private var markerLocation : CGPoint = CGPoint(x: 0, y: 0) - - /// Detects input either from touch of pointer. Finds the nearest data point and displays the relevent information. - /// - Parameters: - /// - specifier: Decimal precision for labels - /// - infoBoxPlacement: Placement of the data point information panel when touch overlay modifier is applied. - internal init(specifier: String, units: Units) { - self.specifier = specifier - self.units = units -======= /** Finds the nearest data point and displays the relevent information. */ @@ -56,7 +22,6 @@ internal struct TouchOverlay: ViewModifier where T: CTChartData { self.chartData = chartData self.chartData.infoView.touchSpecifier = specifier self.chartData.infoView.touchUnit = unit ->>>>>>> version-2 } internal func body(content: Content) -> some View { @@ -71,167 +36,6 @@ internal struct TouchOverlay: ViewModifier where T: CTChartData { chartData.setTouchInteraction(touchLocation: value.location, chartSize: geo.frame(in: .local)) } -<<<<<<< HEAD - - if chartData.chartStyle.infoBoxPlacement == .floating { - setBoxLocationation(boxFrame: boxFrame, chartSize: geo) - markerLocation.x = setMarkerXLocation(chartSize: geo) - markerLocation.y = setMarkerYLocation(chartSize: geo) - } else if chartData.chartStyle.infoBoxPlacement == .header { - chartData.chartStyle.infoBoxPlacement = .header - chartData.viewData.isTouchCurrent = true - chartData.viewData.touchOverlayInfo = selectedPoint - chartData.viewData.units = units - } - } - .onEnded { _ in - isTouchCurrent = false - chartData.viewData.isTouchCurrent = false - } - ) - if isTouchCurrent { - TouchOverlayMarker(position: pointLocation) - .stroke(Color(.gray), lineWidth: touchMarkerLineWidth) - if chartData.chartStyle.infoBoxPlacement == .floating, let lineChartStyle = chartData.lineStyle { - TouchOverlayBox(selectedPoint: selectedPoint, specifier: specifier, units: units, boxFrame: $boxFrame, ignoreZero: lineChartStyle.ignoreZero) - .position(x: boxLocation.x, y: 0 + (boxFrame.height / 2)) - } - } - } - } - } else { content } - } - - // MARK: - Bar Chart - /// Gets the nearest data point to the touch location based on the X axis. - /// - Parameters: - /// - touchLocation: Current location of the touch - /// - chartSize: The size of the chart view as the parent view. - internal func getDataPointLineChart(touchLocation: CGPoint, chartSize: GeometryProxy) /* -> ChartDataPoint */ { - let dataPoints : [ChartDataPoint] = chartData.dataPoints - let xSection : CGFloat = chartSize.size.width / CGFloat(dataPoints.count - 1) - let index = Int((touchLocation.x + (xSection / 2)) / xSection) - if index >= 0 && index < dataPoints.count { - self.selectedPoint = dataPoints[index] - } - } - /// Gets the location of the data point in the view. For Line Chart - /// - Parameters: - /// - touchLocation: Current location of the touch - /// - chartSize: The size of the chart view as the parent view. - internal func getPointLocationLineChart(touchLocation: CGPoint, chartSize: GeometryProxy) /* -> CGPoint */ { - - 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() - } - - let dataPointCount : Int = chartData.dataPoints.count - let xSection : CGFloat = chartSize.size.width / CGFloat(dataPointCount - 1) - let ySection : CGFloat = chartSize.size.height / CGFloat(range) - let index = Int((touchLocation.x + (xSection / 2)) / xSection) - - if index >= 0 && index < dataPointCount { - if !chartData.lineStyle.ignoreZero { - self.pointLocation = CGPoint(x: CGFloat(index) * xSection, - y: (CGFloat(chartData.dataPoints[index].value - minValue) * -ySection) + chartSize.size.height) - } else { - var pointValue : Double - if chartData.dataPoints[index].value == 0 { - if index > 0 && index < chartData.dataPoints.count - 1 { - // Set data point value as halfway between the previous and next value - pointValue = (chartData.dataPoints[index-1].value + chartData.dataPoints[index+1].value) / 2 - } else { - pointValue = chartData.dataPoints[index].value - } - } else { - pointValue = chartData.dataPoints[index].value - } - self.pointLocation = CGPoint(x: CGFloat(index) * xSection, - y: (CGFloat(pointValue - minValue) * -ySection) + chartSize.size.height) - } - } - } - - // MARK: - Bar Chart - /// Gets the nearest data point to the touch location based on the X axis. - /// - Parameters: - /// - touchLocation: Current location of the touch - /// - chartSize: The size of the chart view as the parent view. - internal func getDataPointBarChart(touchLocation: CGPoint, chartSize: GeometryProxy) /* -> ChartDataPoint */ { - let dataPoints : [ChartDataPoint] = chartData.dataPoints - let xSection : CGFloat = chartSize.size.width / CGFloat(dataPoints.count) - let index : Int = Int((touchLocation.x) / xSection) - if index >= 0 && index < dataPoints.count { - self.selectedPoint = dataPoints[index] - } - } - - /// Gets the location of the data point in the view. For BarChart - /// - Parameters: - /// - touchLocation: Current location of the touch - /// - chartSize: The size of the chart view as the parent view. - internal func getPointLocationBarChart(touchLocation: CGPoint, chartSize: GeometryProxy) /* -> CGPoint */ { - - let dataPointCount : Int = chartData.dataPoints.count - let xSection : CGFloat = chartSize.size.width / CGFloat(dataPointCount) - let ySection : CGFloat = chartSize.size.height / CGFloat(chartData.maxValue()) - - let index = Int((touchLocation.x) / xSection) - - if index >= 0 && index < dataPointCount { - self.pointLocation = CGPoint(x: (CGFloat(index) * xSection) + (xSection / 2), - y: (chartSize.size.height - CGFloat(chartData.dataPoints[index].value) * ySection)) - } - } - - // MARK: - Both - /// Sets the 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(boxFrame: CGRect, chartSize: GeometryProxy) { - if touchLocation.x < chartSize.frame(in: .local).minX + (boxFrame.width / 2) { - boxLocation.x = chartSize.frame(in: .local).minX + (boxFrame.width / 2) - } else if touchLocation.x > chartSize.frame(in: .local).maxX - (boxFrame.width / 2) { - boxLocation.x = chartSize.frame(in: .local).maxX - (boxFrame.width / 2) - } else { - boxLocation.x = touchLocation.x - } - } - /// Sets the X axis marker location while keeping it within the parent view. - /// - Parameter chartSize: The size of the chart view as the parent view. - /// - Returns: Position of the marker. - internal func setMarkerXLocation(chartSize: GeometryProxy) -> CGFloat { - if touchLocation.x < chartSize.frame(in: .local).minX { - return chartSize.frame(in: .local).minX - } else if touchLocation.x > chartSize.frame(in: .local).maxX { - return chartSize.frame(in: .local).maxX - } else { - return touchLocation.x - } - } - /// Sets the Y axis marker location while keeping it within the parent view. - /// - Parameter chartSize: The size of the chart view as the parent view. - /// - Returns: Position of the marker. - internal func setMarkerYLocation(chartSize: GeometryProxy) -> CGFloat { - if touchLocation.y < chartSize.frame(in: .local).minY { - return chartSize.frame(in: .local).minY - } else if touchLocation.y > chartSize.frame(in: .local).maxY { - return chartSize.frame(in: .local).maxY - } else { - return touchLocation.y -======= .onEnded { _ in chartData.infoView.isTouchCurrent = false chartData.infoView.touchOverlayInfo = [] @@ -244,7 +48,6 @@ internal struct TouchOverlay: ViewModifier where T: CTChartData { } } } else { content } ->>>>>>> version-2 } } } @@ -252,15 +55,6 @@ internal struct TouchOverlay: ViewModifier where T: CTChartData { 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 { - self.modifier(TouchOverlay(specifier: specifier, units: units)) - } - #elseif os(tvOS) - public func touchOverlay(specifier: String = "%.0f", units: Units = .none) -> some View { -======= /** Adds touch interaction with the chart. @@ -304,7 +98,6 @@ extension View { specifier: String = "%.0f", unit : TouchUnit = .none ) -> some View { ->>>>>>> version-2 self.modifier(EmptyModifier()) } #endif diff --git a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift index 35441a9a..4d5d082c 100644 --- a/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift +++ b/Sources/SwiftUICharts/Shared/Views/TouchOverlayBox.swift @@ -12,65 +12,10 @@ 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 -<<<<<<< HEAD - internal init(selectedPoint : ChartDataPoint?, - specifier : String = "%.0f", - units : Units, - boxFrame : Binding, - ignoreZero : Bool - ) { - self.selectedPoint = selectedPoint - self.specifier = specifier - self.units = units - self._boxFrame = boxFrame - self.ignoreZero = ignoreZero - } - - internal var body: some View { - VStack { - if ignoreZero && selectedPoint?.value != 0 { - switch units { - case .none: - Text("\(selectedPoint?.value ?? 0, specifier: specifier)") - .font(.subheadline) - case .prefix(of: let value): - Text("\(value) \(selectedPoint?.value ?? 0, specifier: specifier)") - .font(.subheadline) - case .suffix(of: let value): - Text("\(selectedPoint?.value ?? 0, specifier: specifier) \(value)") - .font(.subheadline) - } - } else if !ignoreZero { - switch units { - case .none: - Text("\(selectedPoint?.value ?? 0, specifier: specifier)") - .font(.subheadline) - case .prefix(of: let value): - Text("\(value) \(selectedPoint?.value ?? 0, specifier: specifier)") - .font(.subheadline) - case .suffix(of: let value): - Text("\(selectedPoint?.value ?? 0, specifier: specifier) \(value)") - .font(.subheadline) - } - } - if let label = selectedPoint?.pointDescription { - Text(label) - .font(.subheadline) - } else if let label = selectedPoint?.xAxisLabel { - Text(label) - .font(.subheadline) -======= internal init(chartData: T, boxFrame : Binding ) { @@ -95,7 +40,6 @@ internal struct TouchOverlayBox: View { .font(.subheadline) .foregroundColor(chartData.chartStyle.infoBoxDescriptionColour) Spacer() ->>>>>>> version-2 } } diff --git a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift index 2ad55fee..4f33503a 100644 --- a/Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift +++ b/Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift @@ -13,38 +13,6 @@ 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 - - internal init(chartData : ChartData, - markerValue : Double = 0, - isAverage : Bool, - chartType : ChartType - ) { - self.chartData = chartData - self.markerValue = markerValue - self.isAverage = isAverage - self.chartType = chartType - - 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() - } - } - - internal func path(in rect: CGRect) -> Path { - - let value : Double = isAverage ? chartData.average() : markerValue - -======= let range : Double let minValue: Double let maxValue: Double @@ -64,7 +32,6 @@ internal struct Marker: Shape { internal func path(in rect: CGRect) -> Path { ->>>>>>> version-2:Sources/SwiftUICharts/SharedLineAndBar/Shapes/Marker.swift var path = Path() let pointY : CGFloat