Compare commits

...

17 Commits

Author SHA1 Message Date
“Andras d298e31fc2 feat(new-version): add new readme 2023-01-22 21:51:40 +01:00
Andras Samu 7140b8b6fd feat: add animation toggle interface (#256)
Co-authored-by: “Andras <“andras.samu@supercharge.io”>
2022-11-26 14:37:17 +01:00
Andras Samu 7fd5b185f8 Feat/new protocol and range (#255)
* feat: new protocol for chained functions, and added support for explicit Y ranges. X coming as well

* feat: add new axis interface (#253)
2022-11-26 14:34:45 +01:00
Andras Samu ebaaf81d19 feat: new protocol for chained functions, and added support for expli… (#252)
* feat: new protocol for chained functions, and added support for explicit Y ranges. X coming as well

* feat: add new axis interface (#253)
2022-10-24 16:07:51 +02:00
Andras Samu d7e9802deb fix: remove UIColors which caused CI build errors (#251) 2022-09-03 19:01:01 +02:00
Andras Samu bd29afc4c9 fix: BarChartCellShape to handle negative numbers correctly (#250) 2022-09-03 18:55:59 +02:00
Andras Samu caa75ecbc0 feat: add linechart interaction point (#202)
* feat: add linechart interaction point

* feat: add ability to show current data point on linechart
2021-08-11 22:01:43 +02:00
Andras Samu 7861bbcad1 feat(core): refactoring chart dispalying (#191)
now it is possible to add background lines precisely as charts are displayed at correct size
also rewrote basics to conform with Shapes and Animatable protocol
2021-06-09 08:54:04 +02:00
Andras Samu 84578d2f6f Add a public init() to RingsChart 2020-08-24 20:26:37 +02:00
Andras Samu 9210d01137 V2 beta 2 changes (#150)
* Add PieChart interaction PR changes to v2

* Add custom string format for ChartLabel when interactionInProgress = true (#151)

* Dark/Light mode fixes (#148)

Fix for making text work with both Dark/Light mode.

Also solves line chart background to appear white in dark mode

* Add custom string format for ChartLabel when interactionInProgress = true

Co-authored-by: Sagar Patel <s.72427patel@gmail.com>

* Prepare charts to display x and y values souch as a value for a given point

Co-authored-by: Roddy Munro <roddymunro@icloud.com>
Co-authored-by: Sagar Patel <s.72427patel@gmail.com>
2020-08-24 16:51:22 +02:00
Dan Wood 8ee353c93a Activity-type Rings charts (#161)
Co-authored-by: Dan Wood <danwood@users.noreply.github.com>
2020-08-24 16:31:17 +02:00
Dan Wood 51db5a067a Issue 99 documentation (#159)
* Starting on filling in documentation.

* First pass on most/all files

* more descriptions filled in

* Some documentation but TBH the author would be better suited to explain how this works!

* more basic stuff filled in

* Add a description and bunch of discussion text for most of the view `body` declarations

* more explanations

Co-authored-by: Dan Wood <danwood@users.noreply.github.com>
2020-08-24 16:30:30 +02:00
Dan Wood ed01f5305d recalculate geometry if orientation has changed (#156)
Co-authored-by: Dan Wood <danwood@users.noreply.github.com>
2020-08-24 16:24:33 +02:00
Sagar Patel 2ef73c84e2 Dark/Light mode fixes (#148)
Fix for making text work with both Dark/Light mode.

Also solves line chart background to appear white in dark mode
2020-07-31 13:13:56 +02:00
Andras Samu 7fb2a0013c Fix cornerMasking on card view when no shadow is set 2020-07-29 18:49:57 +02:00
Andras Samu 3265d3e16b Add public modifier to ChartColors and add showShadow property 2020-07-25 19:32:05 +02:00
Andras Samu c46902dab8 Refactor Chart base (#143) 2020-07-25 18:56:58 +02:00
88 changed files with 1890 additions and 712 deletions
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>SwiftUICharts.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "91E23D30-CB6C-44DA-BEFC-9D39A1DA2242"
type = "1"
version = "2.0">
</Bucket>
@@ -7,7 +7,20 @@
<key>SwiftUICharts.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
<integer>0</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>SwiftUICharts</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>SwiftUIChartsTests</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
+131 -143
View File
@@ -1,192 +1,180 @@
# SwiftUICharts
Swift package for displaying charts effortlessly.
### SwiftUICharts is here with a new version, giving more controll and customization over the charts and full ios 13 backwards compatibility.
![SwiftUI Charts](./Resources/showcase1.gif "SwiftUI Charts")
SwiftUICharts is an open source library for creating charts in iOS apps with SwiftUI. Built natively with SwiftUI in mind and it is fully compatible with the latest versions of iOS, also backwards **compatible up to iOS 13.**
It supports:
* Line charts
* Bar charts
* Pie charts
With SwiftUICharts, you can easily create a variety of chart types, including line charts, bar charts, ring charts and pie charts. It also provides additional chart types and customization options, giving you the flexibility to create charts that perfectly fit your needs.
### Installation:
In addition to its native SwiftUI integration, backwards compatibility and wide range of chart types, SwiftUICharts is also easy to use, making it a great choice for creating stunning and intuitive charts in your applications.
It requires iOS 13 and Xcode 11!
<p align="center">
<img src="Resources/linevid2.gif" width="30%"/> <img src="Resources/barvid2.gif" width="30%"/> <img src="Resources/pievid2.gif" width="30%"/>
</p>
In Xcode got to `File -> Swift Packages -> Add Package Dependency` and paste inthe repo's url: `https://github.com/AppPear/ChartView`
### Composable
### Usage:
You can now build your custom chart from prebuilt layers. Use a simple chart, add background grids, add chart labels to the axes, add tcks to the line, etc.
import the package in the file you would like to use it: `import SwiftUICharts`
### Works great with native SwiftUI elements
You can display a Chart by adding a chart view to your parent view:
[Example codes here](./example.md)
<p align="left">
<img src="Resources/linechartcard.png" width="40%"/> <img src="Resources/barchartcard.png" width="40%"/>
<img src="Resources/piechartcard.png" width="40%"/>
</p>
### Demo
### Installation
Added an example project, with **iOS, watchOS** target: https://github.com/AppPear/ChartViewDemo
It is a SPM package, in Xcode go to File -> Swift Packages -> Add Package Dependency and paste in the repo's url: https://github.com/AppPear/ChartView
## Line charts
if you want to add previous releases you can find release tags https://github.com/AppPear/ChartView/releases
**LineChartView with multiple lines!**
First release of this feature, interaction is disabled for now, I'll figure it out how could be the best to interact with multiple lines with a single touch.
![Multiine Charts](./Resources/multiline1.gif "Multiine Charts")
### Slack
Usage:
```swift
MultiLineChartView(data: [([8,32,11,23,40,28], GradientColors.green), ([90,99,78,111,70,60,77], GradientColors.purple), ([34,56,72,38,43,100,50], GradientColors.orngPink)], title: "Title")
```
Gradient colors are now under the `GradientColor` struct you can create your own gradient by `GradientColor(start: Color, end: Color)`
Join our Slack channel for day to day conversation and more insights:
Available preset gradients:
* orange
* blue
* green
* blu
* bluPurpl
* purple
* prplPink
* prplNeon
* orngPink
https://join.slack.com/t/swiftuichartview/shared_invite/zt-g6mxioq8-j3iUTF1YKX7D23ML3qcc4g
**Full screen view called LineView!!!**
## Quick start guide:
![Line Charts](./Resources/fullscreen2.gif "Line Charts")
**Create a simple chart:**
<p align="left">
<img src="Resources/chartpic1.png" width="350px"/>
</p>
```swift
LineView(data: [8,23,54,32,12,37,7,23,43], title: "Line chart", legend: "Full screen") // legend is optional, use optional .padding()
LineChart()
.data([3, 5, 4, 1, 0, 2, 4, 1, 0, 2, 8])
.chartStyle(ChartStyle(backgroundColor: .white, foregroundColor: ColorGradient(.orange, .red)))
```
Adopts to dark mode automatically
**Add a background grid to the chart:**
![Line Charts](./Resources/showcase3.gif "Line Charts")
You can add your custom darkmode style by specifying:
<p align="left">
<img src="Resources/chartpic2.png" width="350px"/>
</p>
```swift
let myCustomStyle = ChartStyle(...)
let myCutsomDarkModeStyle = ChartStyle(...)
myCustomStyle.darkModeStyle = myCutsomDarkModeStyle
ChartGrid {
LineChart()
.data([3, 5, 4, 1, 0, 2, 4, 1, 0, 2, 8])
.chartStyle(ChartStyle(backgroundColor: .white, foregroundColor: ColorGradient(.orange, .red)))
}
.setNumberOfHorizontalLines(5)
.setNumberOfVerticalLines(4)
```
**Line chart is interactive, so you can drag across to reveal the data points**
**Add a axis label to the chart:**
You can add a line chart with the following code:
<p align="left">
<img src="Resources/chartpic3.png" width="350px"/>
</p>
```swift
LineChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", legend: "Legendary") // legend is optional
AxisLabels {
ChartGrid {
LineChart()
.data([3, 5, 4, 1, 0, 2, 4, 1, 0, 2, 8])
.chartStyle(ChartStyle(backgroundColor: .white, foregroundColor: ColorGradient(.orange, .red)))
}
.setNumberOfHorizontalLines(5)
.setNumberOfVerticalLines(4)
}
.setAxisXLabels([(1, "Nov"), (2, "Dec"), (3, "Jan")], range: 1...3)
```
**Turn drop shadow off by adding to the Initialiser: `dropShadow: false`**
**Show chart marks on the line:**
## Bar charts
![Bar Charts](./Resources/showcase2.gif "Bar Charts")
**[New feature] you can display labels also along values and points for each bar to descirbe your data better!**
**Bar chart is interactive, so you can drag across to reveal the data points**
You can add a bar chart with the following code:
Labels and points:
<p align="left">
<img src="Resources/chartpic4.png" width="350px"/>
</p>
```swift
BarChartView(data: ChartData(values: [("2018 Q4",63150), ("2019 Q1",50900), ("2019 Q2",77550), ("2019 Q3",79600), ("2019 Q4",92550)]), title: "Sales", legend: "Quarterly") // legend is optional
AxisLabels {
ChartGrid {
LineChart()
.showChartMarks(true)
.data([3, 5, 4, 1, 0, 2, 4, 1, 0, 2, 8])
.chartStyle(ChartStyle(backgroundColor: .white, foregroundColor: ColorGradient(.orange, .red)))
}
.setNumberOfHorizontalLines(5)
.setNumberOfVerticalLines(4)
}
.setAxisXLabels([(1, "Nov"), (2, "Dec"), (3, "Jan")], range: 1...3)
```
Only points:
**Apply custom ranges on chart**
<p align="left">
<img src="Resources/chartpic5.png" width="350px"/>
</p>
```swift
BarChartView(data: ChartData(points: [8,23,54,32,12,37,7,23,43]), title: "Title", legend: "Legendary") // legend is optional
AxisLabels {
ChartGrid {
LineChart()
.showChartMarks(true)
.data([3, 5, 4, 1, 0, 2, 4, 1, 0, 2, 8])
.rangeY(0...10)
.rangeX(0...5)
.chartStyle(ChartStyle(backgroundColor: .white, foregroundColor: ColorGradient(.orange, .red)))
}
.setNumberOfHorizontalLines(5)
.setNumberOfVerticalLines(4)
}
.setAxisXLabels([(1, "Nov"), (2, "Dec"), (3, "Jan")], range: 1...3)
```
**ChartData** structure
Stores values in data pairs (actually tuple): `(String,Double)`
* you can have duplicate values
* keeps the data order
**Add multiple charts in the same frame**
You can initialise ChartData multiple ways:
* For integer values: `ChartData(points: [8,23,54,32,12,37,7,23,43])`
* For floating point values: `ChartData(points: [2.34,3.14,4.56])`
* For label,value pairs: `ChartData(values: [("2018 Q4",63150), ("2019 Q1",50900)])`
You can add different formats:
* Small `ChartForm.small`
* Medium `ChartForm.medium`
* Large `ChartForm.large`
<p align="left">
<img src="Resources/chartpic6.png" width="350px"/>
</p>
```swift
BarChartView(data: ChartData(points: [8,23,54,32,12,37,7,23,43]), title: "Title", form: ChartForm.small)
AxisLabels {
ChartGrid {
LineChart()
.showChartMarks(true)
.data([3, 5, 4, 1, 0, 2, 4, 1, 0, 2, 8])
.rangeY(0...12)
.rangeX(0...5)
.chartStyle(ChartStyle(backgroundColor: .white, foregroundColor: ColorGradient(.orange, .red)))
LineChart()
.showChartMarks(true)
.data([4, 1, 0, 2, 8, 3, 6, 1, 4])
.rangeY(0...12)
.rangeX(0...5)
.chartStyle(ChartStyle(backgroundColor: .white, foregroundColor: ColorGradient(.blue, .purple)))
}
.setNumberOfHorizontalLines(5)
.setNumberOfVerticalLines(4)
}
.setAxisXLabels([(1, "Nov"), (2, "Dec"), (3, "Jan")], range: 1...3)
```
For floating point numbers, you can set a custom specifier:
**Combine chart types in the same frame**
<p align="left">
<img src="Resources/chartpic7.png" width="350px"/>
</p>
```swift
BarChartView(data: ChartData(points:[1.23,2.43,3.37]) ,title: "A", valueSpecifier: "%.2f")
```
For integers you can disable by passing: `valueSpecifier: "%.0f"`
You can set your custom image in the upper right corner by passing in the initialiser: `cornerImage:Image(systemName: "waveform.path.ecg")`
**Turn drop shadow off by adding to the Initialiser: `dropShadow: false`**
### You can customize styling of the chart with a ChartStyle object:
Customizable:
* background color
* accent color
* second gradient color
* text color
* legend text color
```swift
let chartStyle = ChartStyle(backgroundColor: Color.black, accentColor: Colors.OrangeStart, secondGradientColor: Colors.OrangeEnd, chartFormSize: ChartForm.medium, textColor: Color.white, legendTextColor: Color.white )
...
BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", style: chartStyle)
AxisLabels {
ChartGrid {
BarChart()
.data([2, 4, 1, 3])
.chartStyle(ChartStyle(backgroundColor: .white, foregroundColor: ColorGradient(.orange, .red)))
LineChart()
.showChartMarks(true)
.data([2, 4, 1, 3])
.chartStyle(ChartStyle(backgroundColor: .white, foregroundColor: ColorGradient(.blue, .purple)))
}
.setNumberOfHorizontalLines(5)
.setNumberOfVerticalLines(4)
}
.setAxisXLabels([(1, "Nov"), (2, "Dec"), (3, "Jan")], range: 1...3)
```
You can access built-in styles:
```swift
BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", style: Styles.barChartMidnightGreen)
```
#### All styles available as a preset:
* barChartStyleOrangeLight
* barChartStyleOrangeDark
* barChartStyleNeonBlueLight
* barChartStyleNeonBlueDark
* barChartMidnightGreenLight
* barChartMidnightGreenDark
![Midnightgreen](./Resources/midnightgreen.gif "Midnightgreen")
![Custom Charts](./Resources/showcase5.png "Custom Charts")
### You can customize the size of the chart with a ChartForm object:
**ChartForm**
* `.small`
* `.medium`
* `.large`
* `.detail`
```swift
BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", form: ChartForm.small)
```
### WatchOS support for Bar charts:
![Pie Charts](./Resources/watchos1.png "Pie Charts")
## Pie charts
![Pie Charts](./Resources/showcase4.png "Pie Charts")
You can add a pie chart with the following code:
```swift
PieChartView(data: [8,23,54,32], title: "Title", legend: "Legendary") // legend is optional
```
**Turn drop shadow off by adding to the Initialiser: `dropShadow: false`**
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

@@ -0,0 +1,98 @@
import SwiftUI
public struct AxisLabels<Content: View>: View {
struct YAxisViewKey: ViewPreferenceKey { }
struct ChartViewKey: ViewPreferenceKey { }
var axisLabelsData = AxisLabelsData()
var axisLabelsStyle = AxisLabelsStyle()
@State private var yAxisWidth: CGFloat = 25
@State private var chartWidth: CGFloat = 0
@State private var chartHeight: CGFloat = 0
let content: () -> Content
public init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var yAxis: some View {
VStack(spacing: 0.0) {
ForEach(Array(axisLabelsData.axisYLabels.reversed().enumerated()), id: \.element) { index, axisYData in
Text(axisYData)
.font(axisLabelsStyle.axisFont)
.foregroundColor(axisLabelsStyle.axisFontColor)
.frame(height: getYHeight(index: index,
chartHeight: chartHeight,
count: axisLabelsData.axisYLabels.count),
alignment: getYAlignment(index: index, count: axisLabelsData.axisYLabels.count))
}
}
.padding([.leading, .trailing], 4.0)
.background(ViewGeometry<YAxisViewKey>())
.onPreferenceChange(YAxisViewKey.self) { value in
yAxisWidth = value.first?.size.width ?? 0.0
}
}
func xAxis(chartWidth: CGFloat) -> some View {
HStack(spacing: 0.0) {
ForEach(Array(axisLabelsData.axisXLabels.enumerated()), id: \.element) { index, axisXData in
Text(axisXData)
.font(axisLabelsStyle.axisFont)
.foregroundColor(axisLabelsStyle.axisFontColor)
.frame(width: chartWidth / CGFloat(axisLabelsData.axisXLabels.count - 1))
}
}
.frame(height: 24.0, alignment: .top)
}
var chart: some View {
self.content()
.background(ViewGeometry<ChartViewKey>())
.onPreferenceChange(ChartViewKey.self) { value in
chartWidth = value.first?.size.width ?? 0.0
chartHeight = value.first?.size.height ?? 0.0
}
}
public var body: some View {
VStack(spacing: 4.0) {
HStack {
if axisLabelsStyle.axisLabelsYPosition == .leading {
yAxis
} else {
Spacer(minLength: yAxisWidth)
}
chart
if axisLabelsStyle.axisLabelsYPosition == .leading {
Spacer(minLength: yAxisWidth)
} else {
yAxis
}
}
xAxis(chartWidth: chartWidth)
}
}
private func getYHeight(index: Int, chartHeight: CGFloat, count: Int) -> CGFloat {
if index == 0 || index == count - 1 {
return chartHeight / (CGFloat(count - 1) * 2) + 10
}
return chartHeight / CGFloat(count - 1)
}
private func getYAlignment(index: Int, count: Int) -> Alignment {
if index == 0 {
return .top
}
if index == count - 1 {
return .bottom
}
return .center
}
}
@@ -0,0 +1,57 @@
import SwiftUI
extension AxisLabels {
public func setAxisYLabels(_ labels: [String],
position: AxisLabelsYPosition = .leading) -> AxisLabels {
self.axisLabelsData.axisYLabels = labels
self.axisLabelsStyle.axisLabelsYPosition = position
return self
}
public func setAxisXLabels(_ labels: [String]) -> AxisLabels {
self.axisLabelsData.axisXLabels = labels
return self
}
public func setAxisYLabels(_ labels: [(Double, String)],
range: ClosedRange<Int>,
position: AxisLabelsYPosition = .leading) -> AxisLabels {
let overreach = range.overreach + 1
var labelArray = [String](repeating: "", count: overreach)
labels.forEach {
let index = Int($0.0) - range.lowerBound
if labelArray[safe: index] != nil {
labelArray[index] = $0.1
}
}
self.axisLabelsData.axisYLabels = labelArray
self.axisLabelsStyle.axisLabelsYPosition = position
return self
}
public func setAxisXLabels(_ labels: [(Double, String)], range: ClosedRange<Int>) -> AxisLabels {
let overreach = range.overreach + 1
var labelArray = [String](repeating: "", count: overreach)
labels.forEach {
let index = Int($0.0) - range.lowerBound
if labelArray[safe: index] != nil {
labelArray[index] = $0.1
}
}
self.axisLabelsData.axisXLabels = labelArray
return self
}
public func setColor(_ color: Color) -> AxisLabels {
self.axisLabelsStyle.axisFontColor = color
return self
}
public func setFont(_ font: Font) -> AxisLabels {
self.axisLabelsStyle.axisFont = font
return self
}
}
@@ -0,0 +1,11 @@
import Foundation
public enum AxisLabelsYPosition {
case leading
case trailing
}
public enum AxisLabelsXPosition {
case top
case bottom
}
@@ -0,0 +1,11 @@
import SwiftUI
public final class AxisLabelsStyle: ObservableObject {
@Published public var axisFont: Font = .callout
@Published public var axisFontColor: Color = .primary
@Published var axisLabelsYPosition: AxisLabelsYPosition = .leading
@Published var axisLabelsXPosition: AxisLabelsXPosition = .bottom
public init() {
// no-op
}
}
@@ -0,0 +1,10 @@
import SwiftUI
public final class AxisLabelsData: ObservableObject {
@Published public var axisYLabels: [String] = []
@Published public var axisXLabels: [String] = []
public init() {
// no-op
}
}
@@ -1,24 +1,37 @@
import SwiftUI
public struct CardView<Content: View>: View {
@Environment(\.chartStyle) private var chartStyle
/// View containing data and some kind of chart content
public struct CardView<Content: View>: View, ChartBase {
public var chartData = ChartData()
let content: () -> Content
public init(@ViewBuilder content: @escaping () -> Content) {
private var showShadow: Bool
@EnvironmentObject var style: ChartStyle
/// Initialize with view options and a nested `ViewBuilder`
/// - Parameters:
/// - showShadow: should card have a rounded-rectangle shadow around it
/// - content: <#content description#>
public init(showShadow: Bool = true, @ViewBuilder content: @escaping () -> Content) {
self.showShadow = showShadow
self.content = content
}
/// The content and behavior of the `CardView`.
///
///
public var body: some View {
ZStack{
Rectangle()
.fill(Color.white)
.cornerRadius(20)
.shadow(color: Color.gray, radius: 8)
VStack {
if showShadow {
RoundedRectangle(cornerRadius: 20)
.fill(Color.white)
.shadow(color: Color(white: 0.9, opacity: 1), radius: 8)
}
VStack (alignment: .leading) {
self.content()
}
.clipShape(RoundedRectangle(cornerRadius: 20))
.clipShape(RoundedRectangle(cornerRadius: showShadow ? 20 : 0))
}
}
}
@@ -1,19 +0,0 @@
import SwiftUI
struct AnyChartType: ChartType {
private let chartMaker: (ChartType.Data, ChartType.Style) -> AnyView
init<S: ChartType>(_ type: S) {
self.chartMaker = type.makeTypeErasedBody
}
func makeChart(data: ChartType.Data, style: ChartType.Style) -> AnyView {
self.chartMaker(data, style)
}
}
fileprivate extension ChartType {
func makeTypeErasedBody(data: ChartType.Data, style: ChartType.Style) -> AnyView {
AnyView(makeChart(data: data, style: style))
}
}
@@ -0,0 +1,6 @@
import SwiftUI
/// Protocol for any type of chart, to get access to underlying data
public protocol ChartBase: View {
var chartData: ChartData { get }
}
@@ -1,9 +1,74 @@
import SwiftUI
/// An observable wrapper for an array of data for use in any chart
public class ChartData: ObservableObject {
@Published public var data: [Double] = []
@Published public var data: [(Double, Double)] = []
public var rangeY: ClosedRange<Double>?
public var rangeX: ClosedRange<Double>?
public init(_ data: [Double]) {
var points: [Double] {
data.filter { rangeX?.contains($0.0) ?? true }.map { $0.1 }
}
var values: [Double] {
data.filter { rangeX?.contains($0.0) ?? true }.map { $0.0 }
}
var normalisedPoints: [Double] {
let absolutePoints = points.map { abs($0) }
var maxPoint = absolutePoints.max()
if let rangeY = rangeY {
maxPoint = Double(rangeY.overreach)
return points.map { ($0 - rangeY.lowerBound) / (maxPoint ?? 1.0) }
}
return points.map { $0 / (maxPoint ?? 1.0) }
}
var normalisedValues: [Double] {
let absoluteValues = values.map { abs($0) }
var maxValue = absoluteValues.max()
if let rangeX = rangeX {
maxValue = Double(rangeX.overreach)
return values.map { ($0 - rangeX.lowerBound) / (maxValue ?? 1.0) }
}
return values.map { $0 / (maxValue ?? 1.0) }
}
var normalisedData: [(Double, Double)] {
Array(zip(normalisedValues, normalisedPoints))
}
var normalisedYRange: Double {
return rangeY == nil ? (normalisedPoints.max() ?? 0.0) - (normalisedPoints.min() ?? 0.0) : 1
}
var normalisedXRange: Double {
return rangeX == nil ? (normalisedValues.max() ?? 0.0) - (normalisedValues.min() ?? 0.0) : 1
}
var isInNegativeDomain: Bool {
if let rangeY = rangeY {
return rangeY.lowerBound < 0
}
return (points.min() ?? 0.0) < 0
}
/// Initialize with data array
/// - Parameter data: Array of `Double`
public init(_ data: [Double], rangeY: ClosedRange<FloatLiteralType>? = nil) {
self.data = data.enumerated().map{ (index, value) in (Double(index), value) }
self.rangeY = rangeY
}
public init(_ data: [(Double, Double)], rangeY: ClosedRange<FloatLiteralType>? = nil) {
self.data = data
self.rangeY = rangeY
}
public init() {
self.data = []
}
}
@@ -1,11 +0,0 @@
import SwiftUI
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ChartType {
associatedtype Body: View
func makeChart(data: Self.Data, style: Self.Style) -> Self.Body
typealias Data = ChartData
typealias Style = ChartStyle
}
@@ -1,5 +1,6 @@
import SwiftUI
/// Representation of a single data point in a chart that is being observed
public class ChartValue: ObservableObject {
@Published var currentValue: Double = 0
@Published var interactionInProgress: Bool = false
@@ -1,22 +0,0 @@
import SwiftUI
public struct ChartView: View {
@Environment(\.chartType) private var chartType
@Environment(\.chartStyle) private var chartStyle
private var data: ChartData
public init(data: ChartData) {
self.data = data
}
public var body: some View {
self.chartType.makeChart(data: data, style: chartStyle)
}
}
extension ChartView {
public init(points: [Double]) {
self.data = ChartData(points)
}
}
@@ -0,0 +1,10 @@
import SwiftUI
public struct ViewGeometry<T>: View where T: PreferenceKey {
public var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: T.self, value: [ViewSizeData(size: geometry.size)] as! T.Value)
}
}
}
@@ -0,0 +1,15 @@
import SwiftUI
public protocol ViewPreferenceKey: PreferenceKey {
typealias Value = [ViewSizeData]
}
public extension ViewPreferenceKey {
static var defaultValue: [ViewSizeData] {
[]
}
static func reduce(value: inout [ViewSizeData], nextValue: () -> [ViewSizeData]) {
value.append(contentsOf: nextValue())
}
}
@@ -0,0 +1,14 @@
import SwiftUI
public struct ViewSizeData: Identifiable, Equatable, Hashable {
public let id: UUID = UUID()
public let size: CGSize
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
@@ -1,13 +1,10 @@
//
// File.swift
//
//
// Created by Nicolas Savoini on 2020-05-25.
//
import Foundation
extension Array where Element == ColorGradient {
/// <#Description#>
/// - Parameter index: offset in data table
/// - Returns: <#description#>
func rotate(for index: Int) -> ColorGradient {
if self.isEmpty {
return ColorGradient.orangeBright
@@ -20,3 +17,10 @@ extension Array where Element == ColorGradient {
return self[index]
}
}
extension Collection {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
@@ -1,8 +1,14 @@
import SwiftUI
extension CGPoint {
/// Calculate X and Y delta for each data point, based on data min/max and enclosing frame.
/// - Parameters:
/// - frame: Rectangle of enclosing frame
/// - data: array of `Double`
/// - Returns: X and Y delta as a `CGPoint`
static func getStep(frame: CGRect, data: [Double]) -> CGPoint {
let padding: CGFloat = 30.0
let padding: CGFloat = 0
// stepWidth
var stepWidth: CGFloat = 0.0
@@ -32,4 +38,10 @@ extension CGPoint {
return CGPoint(x: stepWidth, y: stepHeight)
}
func denormalize(with geometry: GeometryProxy) -> CGPoint {
let width = geometry.frame(in: .local).width
let height = geometry.frame(in: .local).height
return CGPoint(x: self.x * width, y: self.y * height)
}
}
@@ -1,15 +1,10 @@
//
// CGRect+Extension.swift
// SwiftUICharts
//
// Created by Nicolas Savoini on 2020-05-24.
//
import Foundation
import SwiftUI
extension CGRect {
// Return the coordinate for a rectangle center
/// Midpoint of rectangle
/// - Returns: the coordinate for a rectangle center
public var mid: CGPoint {
return CGPoint(x: self.midX, y: self.midY)
}
@@ -0,0 +1,23 @@
import SwiftUI
extension ChartBase {
public func data(_ data: [Double]) -> some ChartBase {
chartData.data = data.enumerated().map{ (index, value) in (Double(index), value) }
return self
}
public func data(_ data: [(Double, Double)]) -> some ChartBase {
chartData.data = data
return self
}
public func rangeY(_ range: ClosedRange<FloatLiteralType>) -> some ChartBase{
chartData.rangeY = range
return self
}
public func rangeX(_ range: ClosedRange<FloatLiteralType>) -> some ChartBase{
chartData.rangeX = range
return self
}
}
@@ -1,6 +1,8 @@
import SwiftUI
extension Color {
/// Create a `Color` from a hexadecimal representation
/// - Parameter hexString: 3, 6, or 8-character string, with optional (ignored) punctuation such as "#"
init(hexString: String) {
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int = UInt64()
@@ -2,29 +2,29 @@ import SwiftUI
extension Path {
func trimmedPath(for percent: CGFloat) -> Path {
// percent difference between points
let boundsDistance: CGFloat = 0.001
let completion: CGFloat = 1 - boundsDistance
let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
// Start/end points centered around given percentage, but capped if right at the very end
let start = pct > completion ? completion : pct - boundsDistance
let end = pct > completion ? 1 : pct + boundsDistance
return trimmedPath(from: start, to: end)
}
func point(for percent: CGFloat) -> CGPoint {
let path = trimmedPath(for: percent)
return CGPoint(x: path.boundingRect.midX, y: path.boundingRect.midY)
}
func point(to maxX: CGFloat) -> CGPoint {
let total = length
let sub = length(to: maxX)
let percent = sub / total
return point(for: percent)
}
var length: CGFloat {
var ret: CGFloat = 0.0
var start: CGPoint?
@@ -56,7 +56,7 @@ extension Path {
}
return ret
}
func length(to maxX: CGFloat) -> CGFloat {
var ret: CGFloat = 0.0
var start: CGPoint?
@@ -107,14 +107,14 @@ extension Path {
}
return ret
}
static func quadCurvedPathWithPoints(points: [Double], step: CGPoint, globalOffset: Double? = nil) -> Path {
var path = Path()
if points.count < 2 {
return path
}
let offset = globalOffset ?? points.min()!
// guard let offset = points.min() else { return path }
// guard let offset = points.min() else { return path }
var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
path.move(to: point1)
for pointIndex in 1..<points.count {
@@ -126,20 +126,86 @@ extension Path {
}
return path
}
static func quadClosedCurvedPathWithPoints(points: [Double], step: CGPoint, globalOffset: Double? = nil) -> Path {
static func quadCurvedPathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if points.count < 2 {
if data.count < 2 {
return path
}
let offset = globalOffset ?? points.min()!
// guard let offset = points.min() else { return path }
path.move(to: .zero)
var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
var point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
path.move(to: point1)
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
let midPoint = CGPoint.midPointForPoints(firstPoint: point1, secondPoint: point2)
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point1))
path.addQuadCurve(to: point2, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point2))
point1 = point2
}
return path
}
static func drawChartMarkers(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
let filteredData = data.filter { $0.1 <= 1 && $0.1 >= 0 }
if filteredData.count < 1 {
return path
}
let convertedXValues = filteredData.map { CGFloat($0.0) * rect.width }
let convertedYPoints = filteredData.map { CGFloat($0.1) * rect.height }
let markerSize = CGSize(width: 8, height: 8)
for pointIndex in 0..<filteredData.count {
path.addRoundedRect(in: CGRect(origin: CGPoint(x: convertedXValues[pointIndex] - markerSize.width / 2,
y: convertedYPoints[pointIndex] - markerSize.height / 2),
size: markerSize),
cornerSize: CGSize(width: markerSize.width / 2,
height: markerSize.height / 2))
}
return path
}
static func drawGridLines(numberOfHorizontalLines: Int, numberOfVerticalLines: Int, in rect: CGRect) -> Path {
var path = Path()
for index in 0..<numberOfHorizontalLines {
let normalisedSpacing = 1.0 / CGFloat(numberOfHorizontalLines - 1)
let startPoint = CGPoint(x: 0, y: normalisedSpacing * CGFloat(index) * rect.height)
let endPoint = CGPoint(x: rect.width, y: normalisedSpacing * CGFloat(index) * rect.height)
path.move(to: startPoint)
path.addLine(to: endPoint)
}
for index in 0..<numberOfVerticalLines {
let normalisedSpacing = 1.0 / CGFloat(numberOfVerticalLines - 1)
let startPoint = CGPoint(x: normalisedSpacing * CGFloat(index) * rect.width, y: 0)
let endPoint = CGPoint(x: normalisedSpacing * CGFloat(index) * rect.width, y: rect.height)
path.move(to: startPoint)
path.addLine(to: endPoint)
}
return path
}
static func quadClosedCurvedPathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if data.count < 2 {
return path
}
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
path.move(to: CGPoint(x: convertedXValues[0], y: 0))
var point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
path.addLine(to: point1)
for pointIndex in 1..<points.count {
let point2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
let midPoint = CGPoint.midPointForPoints(firstPoint: point1, secondPoint: point2)
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point1))
path.addQuadCurve(to: point2, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point2))
@@ -149,40 +215,45 @@ extension Path {
path.closeSubpath()
return path
}
static func linePathWithPoints(points: [Double], step: CGPoint) -> Path {
static func linePathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if points.count < 2 {
if data.count < 2 {
return path
}
guard let offset = points.min() else {
return path
}
let point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
let point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
path.move(to: point1)
for pointIndex in 1..<points.count {
let point2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
path.addLine(to: point2)
}
return path
}
static func closedLinePathWithPoints(points: [Double], step: CGPoint) -> Path {
static func closedLinePathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if points.count < 2 {
if data.count < 2 {
return path
}
guard let offset = points.min() else {
return path
}
var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
path.move(to: point1)
for pointIndex in 1..<points.count {
point1 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
path.addLine(to: point1)
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
path.move(to: .zero)
let point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
path.addLine(to: point1)
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
path.addLine(to: point2)
}
path.addLine(to: CGPoint(x: point1.x, y: 0))
path.closeSubpath()
return path
}
@@ -194,15 +265,15 @@ extension CGPoint {
let y = self.y + (x - self.x) * a
return CGPoint(x: x, y: y)
}
func line(to: CGPoint) -> CGFloat {
dist(to: to)
}
func line(to: CGPoint, x: CGFloat) -> CGFloat {
dist(to: point(to: to, x: x))
}
func quadCurve(to: CGPoint, control: CGPoint) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
@@ -217,7 +288,7 @@ extension CGPoint {
}
return dist
}
func quadCurve(to: CGPoint, control: CGPoint, x: CGFloat) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
@@ -242,14 +313,14 @@ extension CGPoint {
}
return dist
}
func point(to: CGPoint, t: CGFloat, control: CGPoint) -> CGPoint {
let x = CGPoint.value(x: self.x, y: to.x, t: t, c: control.x)
let y = CGPoint.value(x: self.y, y: to.y, t: t, c: control.y)
return CGPoint(x: x, y: y)
}
func curve(to: CGPoint, control1: CGPoint, control2: CGPoint) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
@@ -266,7 +337,7 @@ extension CGPoint {
return dist
}
func curve(to: CGPoint, control1: CGPoint, control2: CGPoint, x: CGFloat) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
@@ -293,14 +364,14 @@ extension CGPoint {
return dist
}
func point(to: CGPoint, t: CGFloat, control1: CGPoint, control2: CGPoint) -> CGPoint {
let x = CGPoint.value(x: self.x, y: to.x, t: t, control1: control1.x, control2: control2.x)
let y = CGPoint.value(x: self.y, y: to.y, t: t, control1: control1.y, control2: control2.x)
return CGPoint(x: x, y: y)
}
static func value(x: CGFloat, y: CGFloat, t: CGFloat, c: CGFloat) -> CGFloat {
var value: CGFloat = 0.0
// (1-t)^2 * p0 + 2 * (1-t) * t * c1 + t^2 * p1
@@ -309,7 +380,7 @@ extension CGPoint {
value += pow(t, 2) * y
return value
}
static func value(x: CGFloat, y: CGFloat, t: CGFloat, control1: CGFloat, control2: CGFloat) -> CGFloat {
var value: CGFloat = 0.0
// (1-t)^3 * p0 + 3 * (1-t)^2 * t * c1 + 3 * (1-t) * t^2 * c2 + t^3 * p1
@@ -319,24 +390,24 @@ extension CGPoint {
value += pow(t, 3) * y
return value
}
static func getMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint {
return CGPoint(
x: point1.x + (point2.x - point1.x) / 2,
y: point1.y + (point2.y - point1.y) / 2
)
}
func dist(to: CGPoint) -> CGFloat {
return sqrt((pow(self.x - to.x, 2) + pow(self.y - to.y, 2)))
}
static func midPointForPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGPoint {
return CGPoint(
x: (firstPoint.x + secondPoint.x) / 2,
y: (firstPoint.y + secondPoint.y) / 2)
}
static func controlPointForPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGPoint {
var controlPoint = CGPoint.midPointForPoints(firstPoint: firstPoint, secondPoint: secondPoint)
let diffY = abs(secondPoint.y - controlPoint.y)
@@ -0,0 +1,7 @@
import Foundation
public extension ClosedRange where Bound: AdditiveArithmetic {
var overreach: Bound {
self.upperBound - self.lowerBound
}
}
@@ -0,0 +1,17 @@
import SwiftUI
extension Shape {
func fill<Fill: ShapeStyle, Stroke: ShapeStyle>(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View {
self
.stroke(strokeStyle, lineWidth: lineWidth)
.background(self.fill(fillStyle))
}
}
extension InsettableShape {
func fill<Fill: ShapeStyle, Stroke: ShapeStyle>(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View {
self
.strokeBorder(strokeStyle, lineWidth: lineWidth)
.background(self.fill(fillStyle))
}
}
@@ -1,11 +1,17 @@
import SwiftUI
extension View {
public func type<S>(_ type: S) -> some View where S: ChartType {
self.environment(\.chartType, AnyChartType(type))
/// Attach chart style to a View
/// - Parameter style: chart style
/// - Returns: `View` with chart style attached
public func chartStyle(_ style: ChartStyle) -> some View {
self.environmentObject(style)
}
public func style(_ style: ChartStyle) -> some View {
self.environment(\.chartStyle, style)
public func toStandardCoordinateSystem() -> some View {
self
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
}
}
@@ -2,15 +2,23 @@ import SwiftUI
public struct ChartGrid<Content: View>: View {
let content: () -> Content
public var gridOptions = GridOptions()
public init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
public var body: some View {
ZStack{
ZStack {
ChartGridShape(numberOfHorizontalLines: gridOptions.numberOfHorizontalLines,
numberOfVerticalLines: gridOptions.numberOfVerticalLines)
.stroke(gridOptions.color, style: gridOptions.strokeStyle)
if gridOptions.showBaseLine {
ChartGridBaseShape()
.stroke(gridOptions.color, style: gridOptions.baseStrokeStyle)
.rotationEffect(.degrees(180), anchor: .center)
}
self.content()
}
}
}
@@ -0,0 +1,18 @@
import SwiftUI
struct ChartGridBaseShape: Shape {
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
}
}
struct ChartGridBaseShape_Previews: PreviewProvider {
static var previews: some View {
ChartGridBaseShape()
.stroke()
.rotationEffect(.degrees(180), anchor: .center)
}
}
@@ -0,0 +1,28 @@
import SwiftUI
struct ChartGridShape: Shape {
var numberOfHorizontalLines: Int
var numberOfVerticalLines: Int
func path(in rect: CGRect) -> Path {
let path = Path.drawGridLines(numberOfHorizontalLines: numberOfHorizontalLines,
numberOfVerticalLines: numberOfVerticalLines,
in: rect)
return path
}
}
struct ChartGridShape_Previews: PreviewProvider {
static var previews: some View {
Group {
ChartGridShape(numberOfHorizontalLines: 5, numberOfVerticalLines: 0)
.stroke()
.toStandardCoordinateSystem()
ChartGridShape(numberOfHorizontalLines: 4, numberOfVerticalLines: 4)
.stroke()
.toStandardCoordinateSystem()
}
.padding()
}
}
@@ -0,0 +1,31 @@
import SwiftUI
extension ChartGrid {
public func setNumberOfHorizontalLines(_ numberOfLines: Int) -> ChartGrid {
self.gridOptions.numberOfHorizontalLines = numberOfLines
return self
}
public func setNumberOfVerticalLines(_ numberOfLines: Int) -> ChartGrid {
self.gridOptions.numberOfVerticalLines = numberOfLines
return self
}
public func setStoreStyle(_ strokeStyle: StrokeStyle) -> ChartGrid {
self.gridOptions.strokeStyle = strokeStyle
return self
}
public func setColor(_ color: Color) -> ChartGrid {
self.gridOptions.color = color
return self
}
public func showBaseLine(_ show: Bool, with style: StrokeStyle? = nil) -> ChartGrid {
self.gridOptions.showBaseLine = show
if let style = style {
self.gridOptions.baseStrokeStyle = style
}
return self
}
}
@@ -0,0 +1,14 @@
import SwiftUI
public final class GridOptions: ObservableObject {
@Published public var numberOfHorizontalLines: Int = 3
@Published public var numberOfVerticalLines: Int = 3
@Published public var strokeStyle: StrokeStyle = StrokeStyle(lineWidth: 1, lineCap: .round, dash: [5, 10])
@Published public var color: Color = Color(white: 0.85)
@Published public var showBaseLine: Bool = true
@Published public var baseStrokeStyle: StrokeStyle = StrokeStyle(lineWidth: 1.5, lineCap: .round, dash: [5, 0])
public init() {
// no-op
}
}
@@ -1,21 +1,24 @@
import SwiftUI
/// What kind of label - this affects color, size, position of the label
public enum ChartLabelType {
case title
case subTitle
case largeTitle
case custom(size: CGFloat)
case custom(size: CGFloat, padding: EdgeInsets, color: Color)
case legend
}
/// A chart may contain any number of labels in pre-set positions based on their `ChartLabelType`
public struct ChartLabel: View {
@Environment(\.chartValue) private var chartValue: ChartValue
@State var textToDisplay = ""
@State var isInteractionInProgress: Bool = false
@EnvironmentObject var chartValue: ChartValue
@State var textToDisplay:String = ""
var format: String = "%.01f"
private var title: String
/// Label font size
/// - Returns: the font size of the label
private var labelSize: CGFloat {
switch labelType {
case .title:
@@ -26,47 +29,79 @@ public struct ChartLabel: View {
return 24.0
case .largeTitle:
return 38.0
case .custom(let size):
case .custom(let size, _, _):
return size
}
}
private let labelType: ChartLabelType
private var labelColor: Color {
/// Padding around label
/// - Returns: the edge padding to use based on position of the label
private var labelPadding: EdgeInsets {
switch labelType {
case .title:
return .black
return EdgeInsets(top: 16.0, leading: 0, bottom: 0.0, trailing: 8.0)
case .legend:
return .gray
return EdgeInsets(top: 4.0, leading: 0, bottom: 0.0, trailing: 8.0)
case .subTitle:
return .black
return EdgeInsets(top: 8.0, leading: 0, bottom: 0.0, trailing: 8.0)
case .largeTitle:
return .black
case .custom(_):
return .black
return EdgeInsets(top: 24.0, leading: 0, bottom: 0.0, trailing: 8.0)
case .custom(_, let padding, _):
return padding
}
}
public init (_ title: String,
type: ChartLabelType = .title) {
self.title = title
labelType = type
/// Which type (color, size, position) for label
private let labelType: ChartLabelType
/// Foreground color for this label
/// - Returns: Color of label based on its `ChartLabelType`
private var labelColor: Color {
switch labelType {
case .title:
return Color.primary
case .legend:
return Color.secondary
case .subTitle:
return Color.primary
case .largeTitle:
return Color.primary
case .custom(_, _, let color):
return color
}
}
/// Initialize
/// - Parameters:
/// - title: Any `String`
/// - type: Which `ChartLabelType` to use
public init (_ title: String,
type: ChartLabelType = .title,
format: String = "%.01f") {
self.title = title
labelType = type
self.format = format
}
/// The content and behavior of the `ChartLabel`.
///
/// Displays current value if chart is currently being touched along a data point, otherwise the specified text.
public var body: some View {
VStack (alignment: self.isInteractionInProgress ? .center : .leading) {
HStack {
Text(textToDisplay)
.font(.system(size: labelSize))
.bold()
.foregroundColor(self.labelColor)
.padding(self.labelPadding)
.onAppear {
self.textToDisplay = title
self.textToDisplay = self.title
}
.onReceive(self.chartValue.objectWillChange) { _ in
self.textToDisplay = self.chartValue.interactionInProgress ? String(format: "%.01f", self.chartValue.currentValue) : self.title
self.isInteractionInProgress = self.chartValue.interactionInProgress
self.textToDisplay = self.chartValue.interactionInProgress ? String(format: format, self.chartValue.currentValue) : self.title
}
if !self.chartValue.interactionInProgress {
Spacer()
}
}
}
}
@@ -1,25 +1,24 @@
import SwiftUI
public struct ChartStyle {
public let backgroundColor: ColorGradient
public class ChartStyle: ObservableObject {
public let backgroundColor: ColorGradient
public let foregroundColor: [ColorGradient]
public init(backgroundColor: Color, foregroundColor: [ColorGradient]) {
self.backgroundColor = ColorGradient.init(backgroundColor)
self.foregroundColor = foregroundColor
}
public init(backgroundColor: Color, foregroundColor: ColorGradient) {
self.backgroundColor = ColorGradient.init(backgroundColor)
self.foregroundColor = [foregroundColor]
}
public init(backgroundColor: ColorGradient, foregroundColor: ColorGradient) {
self.backgroundColor = backgroundColor
self.foregroundColor = [foregroundColor]
}
public init(backgroundColor: ColorGradient, foregroundColor: [ColorGradient]) {
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
@@ -1,15 +1,15 @@
import SwiftUI
public struct ColorGradient: Equatable {
public let startColor: Color
public let startColor: Color
public let endColor: Color
public init(_ color: Color) {
self.startColor = color
self.endColor = color
}
public init (_ startColor: Color, _ endColor: Color) {
public init(_ startColor: Color, _ endColor: Color) {
self.startColor = startColor
self.endColor = endColor
}
@@ -20,11 +20,6 @@ public struct ColorGradient: Equatable {
}
extension ColorGradient {
/// Convenience method to return a LinearGradient from the ColorGradient
/// - Parameters:
/// - startPoint: starting point
/// - endPoint: ending point
/// - Returns: a Linear gradient
public func linearGradient(from startPoint: UnitPoint, to endPoint: UnitPoint) -> LinearGradient {
return LinearGradient(gradient: self.gradient, startPoint: startPoint, endPoint: endPoint)
}
@@ -1,10 +1,9 @@
import SwiftUI
public enum ChartColors {
// Orange
static let orangeBright = Color(hexString: "#FF782C")
static let orangeDark = Color(hexString: "#EC2301")
public static let orangeBright = Color(hexString: "#FF782C")
public static let orangeDark = Color(hexString: "#EC2301")
static let legendColor: Color = Color(hexString: "#E8E7EA")
static let indicatorKnob: Color = Color(hexString: "#FF57A6")
public static let legendColor: Color = Color(hexString: "#E8E7EA")
public static let indicatorKnob: Color = Color(hexString: "#FF57A6")
}
@@ -1,31 +1,13 @@
import SwiftUI
public struct BarChart: ChartType {
public func makeChart(data: Self.Data, style: Self.Style) -> some View {
BarChartRow(chartData: data, style: style)
public struct BarChart: ChartBase {
public var chartData = ChartData()
@EnvironmentObject var style: ChartStyle
public var body: some View {
BarChartRow(chartData: chartData, style: style)
}
public init() {}
}
struct BarChart_Previews: PreviewProvider {
static var previews: some View {
Group {
BarChart().makeChart(
data: .init([0]),
style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack))
Group {
BarChart().makeChart(
data: .init([1, 2, 3, 5, 1]),
style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack))
}.environment(\.colorScheme, .light)
Group {
BarChart().makeChart(
data: .init([1, 2, 3]),
style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack))
}.environment(\.colorScheme, .dark)
}
}
}
@@ -3,46 +3,31 @@ import SwiftUI
public struct BarChartCell: View {
var value: Double
var index: Int = 0
var width: Float
var numberOfDataPoints: Int
var gradientColor: ColorGradient
var touchLocation: CGFloat
var cellWidth: Double {
return Double(width)/(Double(numberOfDataPoints) * 1.5)
}
@State var firstDisplay: Bool = true
@State private var didCellAppear: Bool = false
public init( value: Double,
index: Int = 0,
width: Float,
numberOfDataPoints: Int,
gradientColor: ColorGradient,
touchLocation: CGFloat) {
self.value = value
self.index = index
self.width = width
self.numberOfDataPoints = numberOfDataPoints
self.gradientColor = gradientColor
self.touchLocation = touchLocation
}
public var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 4)
.fill(gradientColor.linearGradient(from: .bottom, to: .top))
}
.frame(width: CGFloat(self.cellWidth))
.scaleEffect(CGSize(width: 1, height: self.firstDisplay ? 0.0 : self.value), anchor: .bottom)
.onAppear {
self.firstDisplay = false
BarChartCellShape(value: didCellAppear ? value : 0.0)
.fill(gradientColor.linearGradient(from: .bottom, to: .top)) .onAppear {
self.didCellAppear = true
}
.onDisappear {
self.firstDisplay = true
self.didCellAppear = false
}
.transition(.slide)
.animation(Animation.spring().delay(self.touchLocation < 0 || !firstDisplay ? Double(self.index) * 0.04 : 0))
.animation(Animation.spring().delay(self.touchLocation < 0 || !didCellAppear ? Double(self.index) * 0.04 : 0))
}
}
@@ -50,17 +35,18 @@ struct BarChartCell_Previews: PreviewProvider {
static var previews: some View {
Group {
Group {
BarChartCell(value: 0, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
BarChartCell(value: 0, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
.padding(50)
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat())
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat())
BarChartCell(value: 0.5, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
BarChartCell(value: 0.75, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat())
BarChartCell(value: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat())
}
Group {
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat())
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat())
BarChartCell(value: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
BarChartCell(value: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat())
BarChartCell(value: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat())
}.environment(\.colorScheme, .dark)
}
}
@@ -0,0 +1,56 @@
import SwiftUI
struct BarChartCellShape: Shape, Animatable {
var value: Double
var cornerRadius: CGFloat = 6.0
var animatableData: CGFloat {
get { CGFloat(value) }
set { value = Double(newValue) }
}
func path(in rect: CGRect) -> Path {
let adjustedOriginY = rect.height - (rect.height * CGFloat(value))
var path = Path()
guard value != 0 else {
return path
}
path.move(to: CGPoint(x: 0.0 , y: rect.height))
path.addLine(to: CGPoint(x: 0.0, y: adjustedOriginY + cornerRadius))
path.addArc(center: CGPoint(x: cornerRadius, y: adjustedOriginY + cornerRadius),
radius: cornerRadius,
startAngle: Angle(radians: Double.pi),
endAngle: Angle(radians: value < 0 ? Double.pi/2 : -Double.pi/2),
clockwise: value < 0 ? true : false)
path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: value < 0 ? adjustedOriginY + 2 * cornerRadius : adjustedOriginY))
path.addArc(center: CGPoint(x: rect.width - cornerRadius, y: adjustedOriginY + cornerRadius),
radius: cornerRadius,
startAngle: Angle(radians: value < 0 ? Double.pi/2 : -Double.pi/2),
endAngle: Angle(radians: 0),
clockwise: value < 0 ? true : false)
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.closeSubpath()
return path
}
}
struct BarChartCellShape_Previews: PreviewProvider {
static var previews: some View {
Group {
BarChartCellShape(value: 0.75)
.fill(Color.red)
BarChartCellShape(value: 0.3)
.fill(Color.blue)
BarChartCellShape(value: 0)
.fill(Color.blue)
.padding(50)
BarChartCellShape(value: -0.3)
.fill(Color.blue)
.offset(x: 0, y: -600)
}
}
}
@@ -1,18 +1,14 @@
import SwiftUI
public struct BarChartRow: View {
@Environment(\.chartValue) private var chartValue: ChartValue
@EnvironmentObject var chartValue: ChartValue
@ObservedObject var chartData: ChartData
@State var touchLocation: CGFloat = -1.0
enum Constant {
static let spacing: CGFloat = 16.0
}
@State private var touchLocation: CGFloat = -1.0
var style: ChartStyle
var maxValue: Double {
guard let max = chartData.data.max() else {
guard let max = chartData.points.max() else {
return 1
}
return max != 0 ? max : 1
@@ -21,25 +17,23 @@ public struct BarChartRow: View {
public var body: some View {
GeometryReader { geometry in
HStack(alignment: .bottom,
spacing: (geometry.frame(in: .local).width - Constant.spacing) / CGFloat(self.chartData.data.count * 3)) {
ForEach(0..<self.chartData.data.count, id: \.self) { index in
BarChartCell(value: self.normalizedValue(index: index),
spacing: geometry.frame(in: .local).width / CGFloat(chartData.data.count * 3)) {
ForEach(0..<chartData.data.count, id: \.self) { index in
BarChartCell(value: chartData.normalisedPoints[index],
index: index,
width: Float(geometry.frame(in: .local).width - Constant.spacing),
numberOfDataPoints: self.chartData.data.count,
gradientColor: self.style.foregroundColor.rotate(for: index),
touchLocation: self.touchLocation)
.scaleEffect(self.getScaleSize(touchLocation: self.touchLocation, index: index), anchor: .bottom)
.animation(Animation.easeIn(duration: 0.2))
}
// .drawingGroup()
// .drawingGroup()
}
.padding([.top, .leading, .trailing], 10)
.frame(maxHeight: chartData.isInNegativeDomain ? geometry.size.height / 2 : geometry.size.height)
.gesture(DragGesture()
.onChanged({ value in
let width = geometry.frame(in: .local).width
self.touchLocation = value.location.x/width
if let currentValue = getCurrentValue(width: width) {
if let currentValue = self.getCurrentValue(width: width) {
self.chartValue.currentValue = currentValue
self.chartValue.interactionInProgress = true
}
@@ -51,10 +45,6 @@ public struct BarChartRow: View {
)
}
}
func normalizedValue(index: Int) -> Double {
return Double(chartData.data[index])/Double(maxValue)
}
func getScaleSize(touchLocation: CGFloat, index: Int) -> CGSize {
if touchLocation > CGFloat(index)/CGFloat(chartData.data.count) &&
@@ -67,28 +57,14 @@ public struct BarChartRow: View {
func getCurrentValue(width: CGFloat) -> Double? {
guard self.chartData.data.count > 0 else { return nil}
let index = max(0,min(self.chartData.data.count-1,Int(floor((self.touchLocation*width)/(width/CGFloat(self.chartData.data.count))))))
return self.chartData.data[index]
return self.chartData.points[index]
}
}
//struct BarChartRow_Previews: PreviewProvider {
// static var previews: some View {
// Group {
// BarChartRow(data: [0], style: styleGreenRed)
// Group {
// BarChartRow(data: [1, 2, 3], style: styleGreenRed)
// BarChartRow(data: [1, 2, 3], style: styleGreenRedWhiteBlack)
// }
// Group {
// BarChartRow(data: [1, 2, 3], style: styleGreenRed)
// BarChartRow(data: [1, 2, 3], style: styleGreenRedWhiteBlack)
// }.environment(\.colorScheme, .dark)
// }
// }
//}
//
//private let styleGreenRed = ChartStyle(backgroundColor: .white, foregroundColor: .greenRed)
//
//private let styleGreenRedWhiteBlack = ChartStyle(
// backgroundColor: ColorGradient.init(.white),
// foregroundColor: [ColorGradient.redBlack, ColorGradient.whiteBlack])
struct BarChartRow_Previews: PreviewProvider {
static let chartData = ChartData([6, 2, 0, 8, 6])
static let chartStyle = ChartStyle(backgroundColor: .white, foregroundColor: .orangeBright)
static var previews: some View {
BarChartRow(chartData: chartData, style: chartStyle)
}
}
@@ -0,0 +1,29 @@
import SwiftUI
extension LineChart {
public func setLineWidth(width: CGFloat) -> LineChart {
self.chartProperties.lineWidth = width
return self
}
public func setBackground(colorGradient: ColorGradient) -> LineChart {
self.chartProperties.backgroundGradient = colorGradient
return self
}
public func showChartMarks(_ show: Bool, with color: ColorGradient? = nil) -> LineChart {
self.chartProperties.showChartMarks = show
self.chartProperties.customChartMarksColors = color
return self
}
public func setLineStyle(to style: LineStyle) -> LineChart {
self.chartProperties.lineStyle = style
return self
}
public func withAnimation(_ enabled: Bool) -> LineChart {
self.chartProperties.animationEnabled = enabled
return self
}
}
@@ -1,15 +1,7 @@
//
// IndicatorPoint.swift
// LineChart
//
// Created by András Samu on 2019. 09. 03..
// Copyright © 2019. András Samu. All rights reserved.
//
import SwiftUI
struct IndicatorPoint: View {
var body: some View {
public var body: some View {
ZStack {
Circle()
.fill(ChartColors.indicatorKnob)
@@ -1,75 +1,64 @@
import SwiftUI
/// A single line of data, a view in a `LineChart`
public struct Line: View {
@Environment(\.chartValue) private var chartValue: ChartValue
@State var frame: CGRect = .zero
@ObservedObject var chartData: ChartData
@ObservedObject var chartProperties: LineChartProperties
var curvedLines: Bool = true
var style: ChartStyle
@State var showIndicator: Bool = false
@State var touchLocation: CGPoint = .zero
@State private var showFull: Bool = false
@State var showBackground: Bool = true
var curvedLines: Bool = true
var step: CGPoint {
return CGPoint.getStep(frame: frame, data: chartData.data)
}
@State private var showIndicator: Bool = false
@State private var touchLocation: CGPoint = .zero
@State private var didCellAppear: Bool = false
var path: Path {
let points = chartData.data
if curvedLines {
return Path.quadCurvedPathWithPoints(points: points,
step: step,
globalOffset: nil)
}
return Path.linePathWithPoints(points: points, step: step)
Path.quadCurvedPathWithPoints(points: chartData.normalisedPoints,
step: CGPoint(x: 1.0, y: 1.0))
}
var closedPath: Path {
let points = chartData.data
if curvedLines {
return Path.quadClosedCurvedPathWithPoints(points: points,
step: step,
globalOffset: nil)
}
return Path.closedLinePathWithPoints(points: points, step: step)
public init(chartData: ChartData,
style: ChartStyle,
chartProperties: LineChartProperties) {
self.chartData = chartData
self.style = style
self.chartProperties = chartProperties
}
public var body: some View {
GeometryReader { geometry in
ZStack {
if self.showFull && self.showBackground {
self.getBackgroundPathView()
}
self.getLinePathView()
if self.showIndicator {
IndicatorPoint()
.position(self.getClosestPointOnPath(touchLocation: self.touchLocation))
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
if self.didCellAppear, let backgroundColor = chartProperties.backgroundGradient {
LineBackgroundShapeView(chartData: chartData,
geometry: geometry,
backgroundColor: backgroundColor)
}
lineShapeView(geometry: geometry)
}
.onAppear {
self.frame = geometry.frame(in: .local)
didCellAppear = true
}
.gesture(DragGesture()
.onChanged({ value in
self.touchLocation = value.location
self.showIndicator = true
self.getClosestDataPoint(point: self.getClosestPointOnPath(touchLocation: value.location))
self.chartValue.interactionInProgress = true
})
.onEnded({ value in
self.touchLocation = .zero
self.showIndicator = false
self.chartValue.interactionInProgress = false
})
)
.onDisappear() {
didCellAppear = false
}
}
}
@ViewBuilder
private func lineShapeView(geometry: GeometryProxy) -> some View {
if chartProperties.animationEnabled {
LineShapeView(chartData: chartData,
chartProperties: chartProperties,
geometry: geometry,
style: style,
trimTo: didCellAppear ? 1.0 : 0.0)
.animation(Animation.easeIn(duration: 0.75))
} else {
LineShapeView(chartData: chartData,
chartProperties: chartProperties,
geometry: geometry,
style: style,
trimTo: 1.0)
}
}
}
@@ -77,55 +66,46 @@ public struct Line: View {
// MARK: - Private functions
extension Line {
private func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint {
let closest = self.path.point(to: touchLocation.x)
return closest
/// Calculate point closest to where the user touched
/// - Parameter touchLocation: location in view where touched
/// - Returns: `CGPoint` of data point on chart
private func getClosestPointOnPath(geometry: GeometryProxy, touchLocation: CGPoint) -> CGPoint {
let geometryWidth = geometry.frame(in: .local).width
let normalisedTouchLocationX = (touchLocation.x / geometryWidth) * CGFloat(chartData.normalisedPoints.count - 1)
let closest = self.path.point(to: normalisedTouchLocationX)
var denormClosest = closest.denormalize(with: geometry)
denormClosest.x = denormClosest.x / CGFloat(chartData.normalisedPoints.count - 1)
denormClosest.y = denormClosest.y / CGFloat(chartData.normalisedYRange)
return denormClosest
}
private func getClosestDataPoint(point: CGPoint) {
let index = Int(round((point.x)/step.x))
// /// Figure out where closest touch point was
// /// - Parameter point: location of data point on graph, near touch location
private func getClosestDataPoint(geometry: GeometryProxy, touchLocation: CGPoint) {
let geometryWidth = geometry.frame(in: .local).width
let index = Int(round((touchLocation.x / geometryWidth) * CGFloat(chartData.points.count - 1)))
if (index >= 0 && index < self.chartData.data.count){
self.chartValue.currentValue = self.chartData.data[index]
// self.chartValue.currentValue = self.chartData.points[index]
}
}
private func getBackgroundPathView() -> some View {
self.closedPath
.fill(style.backgroundColor.linearGradient(from: .bottom, to: .top))
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
.transition(.opacity)
.animation(.easeIn(duration: 1.6))
}
private func getLinePathView() -> some View {
self.path
.trim(from: 0, to: self.showFull ? 1:0)
.stroke(LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient,
startPoint: .leading,
endPoint: .trailing),
style: StrokeStyle(lineWidth: 3, lineJoin: .round))
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
.animation(Animation.easeOut(duration: 1.2))
.onAppear {
self.showFull = true
}
.onDisappear {
self.showFull = false
}
.drawingGroup()
}
}
struct Line_Previews: PreviewProvider {
/// Predefined style, black over white, for preview
static let blackLineStyle = ChartStyle(backgroundColor: ColorGradient(.white), foregroundColor: ColorGradient(.black))
/// Predefined style red over white, for preview
static let redLineStyle = ChartStyle(backgroundColor: .whiteBlack, foregroundColor: ColorGradient(.red))
static var previews: some View {
Group {
Line(chartData: ChartData([8, 23, 32, 7, 23, 43]), style: blackLineStyle)
Line(chartData: ChartData([8, 23, 32, 7, 23, 43]), style: redLineStyle)
Line(chartData: ChartData([8, 23, 32, 7, 23, -4]),
style: blackLineStyle,
chartProperties: LineChartProperties())
Line(chartData: ChartData([8, 23, 32, 7, 23, 43]),
style: redLineStyle,
chartProperties: LineChartProperties())
}
}
}
private let blackLineStyle = ChartStyle(backgroundColor: ColorGradient(.white), foregroundColor: ColorGradient(.black))
private let redLineStyle = ChartStyle(backgroundColor: .whiteBlack, foregroundColor: ColorGradient(.red))
@@ -0,0 +1,27 @@
import SwiftUI
struct LineBackgroundShape: Shape {
var data: [(Double, Double)]
func path(in rect: CGRect) -> Path {
let path = Path.quadClosedCurvedPathWithPoints(data: data, in: rect)
return path
}
}
struct LineBackgroundShape_Previews: PreviewProvider {
static var previews: some View {
Group {
GeometryReader { geometry in
LineBackgroundShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)])
.fill(Color.red)
.toStandardCoordinateSystem()
}
GeometryReader { geometry in
LineBackgroundShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)])
.fill(Color.blue)
.toStandardCoordinateSystem()
}
}
}
}
@@ -0,0 +1,16 @@
import SwiftUI
struct LineBackgroundShapeView: View {
var chartData: ChartData
var geometry: GeometryProxy
var backgroundColor: ColorGradient
var body: some View {
LineBackgroundShape(data: chartData.normalisedData)
.fill(LinearGradient(gradient: Gradient(colors: [backgroundColor.startColor,
backgroundColor.endColor]),
startPoint: .bottom,
endPoint: .top))
.toStandardCoordinateSystem()
}
}
@@ -1,32 +1,15 @@
import SwiftUI
public struct LineChart: ChartType {
public func makeChart(data: Self.Data, style: Self.Style) -> some View {
Line(chartData: data, style: style)
}
public struct LineChart: ChartBase {
public var chartData = ChartData()
@EnvironmentObject var style: ChartStyle
public var chartProperties = LineChartProperties()
public var body: some View {
Line(chartData: chartData,
style: style,
chartProperties: chartProperties)
}
public init() {}
}
struct LineChart_Previews: PreviewProvider {
static var previews: some View {
Group {
LineChart().makeChart(
data: .init([0]),
style: .init(backgroundColor: .white, foregroundColor: ColorGradient(.black)))
Group {
LineChart().makeChart(
data: .init([1, 2, 3, 5, 1]),
style: .init(backgroundColor: .white, foregroundColor: ColorGradient(.black)))
}.environment(\.colorScheme, .light)
Group {
LineChart().makeChart(
data: .init([1, 2, 3]),
style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack))
}.environment(\.colorScheme, .dark)
}
}
}
@@ -0,0 +1,30 @@
import SwiftUI
struct LineShape: Shape {
var data: [(Double, Double)]
var lineStyle: LineStyle = .curved
func path(in rect: CGRect) -> Path {
var path = Path()
switch lineStyle {
case .curved:
path = Path.quadCurvedPathWithPoints(data: data, in: rect)
case .straight:
path = Path.linePathWithPoints(data: data, in: rect)
}
return path
}
}
struct LineShape_Previews: PreviewProvider {
static var previews: some View {
Group {
LineShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)])
.stroke()
.toStandardCoordinateSystem()
LineShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)], lineStyle: .straight)
.stroke()
.toStandardCoordinateSystem()
}
}
}
@@ -0,0 +1,83 @@
import SwiftUI
struct LineShapeView: View, Animatable {
var chartData: ChartData
var chartProperties: LineChartProperties
var geometry: GeometryProxy
var style: ChartStyle
var trimTo: Double = 0
var animatableData: CGFloat {
get { CGFloat(trimTo) }
set { trimTo = Double(newValue) }
}
var chartMarkColor: LinearGradient {
if let customColor = chartProperties.customChartMarksColors {
return customColor.linearGradient(from: .leading, to: .trailing)
}
return LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient,
startPoint: .leading,
endPoint: .trailing)
}
var body: some View {
ZStack {
LineShape(data: chartData.normalisedData, lineStyle: chartProperties.lineStyle)
.trim(from: 0, to: CGFloat(trimTo))
.stroke(LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient,
startPoint: .leading,
endPoint: .trailing),
style: StrokeStyle(lineWidth: chartProperties.lineWidth, lineJoin: .round))
.toStandardCoordinateSystem()
.clipped()
if chartProperties.showChartMarks {
MarkerShape(data: chartData.normalisedData)
.trim(from: 0, to: CGFloat(trimTo))
.fill(.white,
strokeBorder: chartMarkColor,
lineWidth: chartProperties.lineWidth)
.toStandardCoordinateSystem()
}
}
}
}
struct LineShapeView_Previews: PreviewProvider {
static let chartData = ChartData([6, 8, 6], rangeY: 6...10)
static let chartDataOutOfRange = ChartData([-1, 8, 6, 12, 3], rangeY: -5...15)
static let chartDataOutOfRange2 = ChartData([6,6,8,5], rangeY: 5...10)
static let chartStyle = ChartStyle(backgroundColor: Color.white,
foregroundColor: [ColorGradient(Color.orange, Color.red)])
static var previews: some View {
Group {
GeometryReader { geometry in
LineShapeView(chartData: chartData,
chartProperties: LineChartProperties(),
geometry: geometry,
style: chartStyle,
trimTo: 1.0)
}
GeometryReader { geometry in
LineShapeView(chartData: chartDataOutOfRange,
chartProperties: LineChartProperties(),
geometry: geometry,
style: chartStyle,
trimTo: 1.0)
}
GeometryReader { geometry in
LineShapeView(chartData: chartDataOutOfRange2,
chartProperties: LineChartProperties(),
geometry: geometry,
style: chartStyle,
trimTo: 1.0)
}
}
}
}
@@ -0,0 +1,23 @@
import SwiftUI
struct MarkerShape: Shape {
var data: [(Double, Double)]
func path(in rect: CGRect) -> Path {
let path = Path.drawChartMarkers(data: data, in: rect)
return path
}
}
struct MarkerShape_Previews: PreviewProvider {
static var previews: some View {
Group {
MarkerShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)])
.stroke()
.toStandardCoordinateSystem()
MarkerShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)])
.stroke()
.toStandardCoordinateSystem()
}
}
}
@@ -0,0 +1,13 @@
import SwiftUI
public class LineChartProperties: ObservableObject {
@Published var lineWidth: CGFloat = 2.0
@Published var backgroundGradient: ColorGradient?
@Published var showChartMarks: Bool = false
@Published var customChartMarksColors: ColorGradient?
@Published var lineStyle: LineStyle = .curved
@Published var animationEnabled: Bool = true
public init() {
// no-op
}
}
@@ -0,0 +1,6 @@
import Foundation
public enum LineStyle {
case curved
case straight
}
@@ -1,63 +1,17 @@
//
// PieChart.swift
// SwiftUICharts
//
// Created by Nicolas Savoini on 2020-05-24.
//
import SwiftUI
public struct PieChart: ChartType {
public func makeChart(data: Self.Data, style: Self.Style) -> some View {
PieChartRow(chartData: data, style: style)
/// A type of chart that displays a slice of "pie" for each data point
public struct PieChart: ChartBase {
public var chartData = ChartData()
@EnvironmentObject var style: ChartStyle
/// The content and behavior of the `PieChart`.
///
///
public var body: some View {
PieChartRow(chartData: chartData, style: style)
}
public init() {}
}
struct PieChart_Previews: PreviewProvider {
static var previews: some View {
Group {
PieChart().makeChart(
data: .init([0]),
style: styleOneColor)
Group {
PieChart().makeChart(
data: .init([56, 78, 53, 65, 54]),
style: styleOneColor)
PieChart().makeChart(
data: .init([56, 78, 53, 65, 54]),
style: styleTwoColor)
PieChart().makeChart(
data: .init([1, 1, 1, 1, 1, 1]),
style: trivialPursuit)
}.environment(\.colorScheme, .light)
Group {
PieChart().makeChart(
data: .init([56, 78, 53, 65, 54]),
style: styleOneColor)
PieChart().makeChart(
data: .init([56, 78, 53, 65, 54]),
style: styleTwoColor)
PieChart().makeChart(
data: .init([1, 1, 1, 1, 1, 1]),
style: trivialPursuit)
}.environment(\.colorScheme, .dark)
}.previewLayout(.fixed(width: 250, height: 400))
}
}
private let styleOneColor = ChartStyle(backgroundColor: .white, foregroundColor: ColorGradient.init(.pink))
private let styleTwoColor = ChartStyle(backgroundColor: ColorGradient(.black), foregroundColor: [ColorGradient(.yellow), ColorGradient(.red)])
private let trivialPursuit = ChartStyle(
backgroundColor: .yellow,
foregroundColor: [ColorGradient(.yellow),
ColorGradient(.pink),
ColorGradient(.green),
ColorGradient(.primary),
ColorGradient(.blue),
ColorGradient(.orange)])
@@ -1,12 +1,6 @@
//
// PieChartCell.swift
// SwiftUICharts
//
// Created by Nicolas Savoini on 2020-05-24.
//
import SwiftUI
/// One slice of a `PieChartRow`
struct PieSlice: Identifiable {
var id = UUID()
var startDeg: Double
@@ -14,6 +8,7 @@ struct PieSlice: Identifiable {
var value: Double
}
/// A single row of data, a view in a `PieChart`
public struct PieChartCell: View {
@State private var show: Bool = false
var rect: CGRect
@@ -21,7 +16,9 @@ public struct PieChartCell: View {
return min(rect.width, rect.height)/2
}
var startDeg: Double
var endDeg: Double
var endDeg: Double
/// Path representing this slice
var path: Path {
var path = Path()
path.addArc(
@@ -42,6 +39,9 @@ public struct PieChartCell: View {
// Section color
var accentColor: ColorGradient
/// The content and behavior of the `PieChartCell`.
///
/// Fills and strokes with 2-pixel line (unless start/end degrees not yet set). Animates by scaling up to 100% when first appears.
public var body: some View {
Group {
path
@@ -0,0 +1,34 @@
import SwiftUI
func isPointInCircle(point: CGPoint, circleRect: CGRect) -> Bool {
let r = min(circleRect.width, circleRect.height) / 2
let center = CGPoint(x: circleRect.midX, y: circleRect.midY)
let dx = point.x - center.x
let dy = point.y - center.y
let distance = sqrt(dx * dx + dy * dy)
return distance <= r
}
func degree(for point: CGPoint, inCircleRect circleRect: CGRect) -> Double {
let center = CGPoint(x: circleRect.midX, y: circleRect.midY)
let dx = point.x - center.x
let dy = point.y - center.y
let acuteDegree = Double(atan(dy / dx)) * (180 / .pi)
let isInBottomRight = dx >= 0 && dy >= 0
let isInBottomLeft = dx <= 0 && dy >= 0
let isInTopLeft = dx <= 0 && dy <= 0
let isInTopRight = dx >= 0 && dy <= 0
if isInBottomRight {
return acuteDegree
} else if isInBottomLeft {
return 180 - abs(acuteDegree)
} else if isInTopLeft {
return 180 + abs(acuteDegree)
} else if isInTopRight {
return 360 - abs(acuteDegree)
}
return 0
}
@@ -1,23 +1,18 @@
//
// PieChartRow.swift
// SwiftUICharts
//
// Created by Nicolas Savoini on 2020-05-24.
//
import SwiftUI
/// A single "row" (slice) of data, a view in a `PieChart`
public struct PieChartRow: View {
@ObservedObject var chartData: ChartData
@EnvironmentObject var chartValue: ChartValue
var style: ChartStyle
var slices: [PieSlice] {
var tempSlices: [PieSlice] = []
var lastEndDeg: Double = 0
let maxValue: Double = chartData.data.reduce(0, +)
let maxValue: Double = chartData.points.reduce(0, +)
for slice in chartData.data {
for slice in chartData.points {
let normalized: Double = Double(slice) / (maxValue == 0 ? 1 : maxValue)
let startDeg = lastEndDeg
let endDeg = lastEndDeg + (normalized * 360)
@@ -27,55 +22,48 @@ public struct PieChartRow: View {
return tempSlices
}
@State private var currentTouchedIndex = -1 {
didSet {
if oldValue != currentTouchedIndex {
chartValue.interactionInProgress = currentTouchedIndex != -1
guard currentTouchedIndex != -1 else { return }
chartValue.currentValue = slices[currentTouchedIndex].value
}
}
}
public var body: some View {
GeometryReader { geometry in
ZStack {
ForEach(0..<self.slices.count) { index in
PieChartCell(
rect: geometry.frame(in: .local),
startDeg: self.slices[index].startDeg,
endDeg: self.slices[index].endDeg,
index: index,
backgroundColor: self.style.backgroundColor.startColor,
accentColor: self.style.foregroundColor.rotate(for: index)
)
rect: geometry.frame(in: .local),
startDeg: self.slices[index].startDeg,
endDeg: self.slices[index].endDeg,
index: index,
backgroundColor: self.style.backgroundColor.startColor,
accentColor: self.style.foregroundColor.rotate(for: index)
)
.scaleEffect(currentTouchedIndex == index ? 1.1 : 1)
.animation(Animation.spring())
}
}
.gesture(DragGesture()
.onChanged({ value in
let rect = geometry.frame(in: .local)
let isTouchInPie = isPointInCircle(point: value.location, circleRect: rect)
if isTouchInPie {
let touchDegree = degree(for: value.location, inCircleRect: rect)
currentTouchedIndex = slices.firstIndex(where: { $0.startDeg < touchDegree && $0.endDeg > touchDegree }) ?? -1
} else {
currentTouchedIndex = -1
}
})
.onEnded({ value in
currentTouchedIndex = -1
})
)
}
}
}
struct PieChartRow_Previews: PreviewProvider {
static var previews: some View {
Group {
//Empty Array - Default Colors.OrangeStart
PieChartRow(
chartData: ChartData([8, 23, 32, 7, 23, 43]),
style: defaultMultiColorChartStyle)
.frame(width: 100, height: 100)
PieChartRow(
chartData: ChartData([8, 23, 32, 7, 23, 43]),
style: multiColorChartStyle)
.frame(width: 100, height: 100)
PieChartRow(
chartData: ChartData([8, 23, 32, 7, 23, 43]),
style: multiColorChartStyle)
.frame(width: 100, height: 100)
}.previewLayout(.fixed(width: 125, height: 125))
}
}
private let defaultMultiColorChartStyle = ChartStyle(
backgroundColor: Color.white,
foregroundColor: [ColorGradient]())
private let multiColorChartStyle = ChartStyle(
backgroundColor: Color.purple,
foregroundColor: [ColorGradient.greenRed, ColorGradient.whiteBlack])
@@ -0,0 +1,187 @@
//
// Ring.swift
// ChartViewV2Demo
//
// Created by Dan Wood on 8/20/20.
// Based on article and playground code by Frank Jia
// https://medium.com/@frankjia/creating-activity-rings-in-swiftui-11ef7d336676
import SwiftUI
extension Double {
func toRadians() -> Double {
return self * Double.pi / 180
}
func toCGFloat() -> CGFloat {
return CGFloat(self)
}
}
struct RingShape: Shape {
/// Helper function to convert percent values to angles in degrees
/// - Parameters:
/// - percent: percent, greater than 100 is OK
/// - startAngle: angle to add after converting
/// - Returns: angle in degrees
static func percentToAngle(percent: Double, startAngle: Double) -> Double {
(percent / 100 * 360) + startAngle
}
private var percent: Double
private var startAngle: Double
private let drawnClockwise: Bool
// This allows animations to run smoothly for percent values
var animatableData: Double {
get {
return percent
}
set {
percent = newValue
}
}
init(percent: Double = 100, startAngle: Double = -90, drawnClockwise: Bool = false) {
self.percent = percent
self.startAngle = startAngle
self.drawnClockwise = drawnClockwise
}
/// This draws a simple arc from the start angle to the end angle
///
/// - Parameter rect: The frame of reference for describing this shape.
/// - Returns: A path that describes this shape.
func path(in rect: CGRect) -> Path {
let width = rect.width
let height = rect.height
let radius = min(width, height) / 2
let center = CGPoint(x: width / 2, y: height / 2)
let endAngle = Angle(degrees: RingShape.percentToAngle(percent: self.percent, startAngle: self.startAngle))
return Path { path in
path.addArc(center: center, radius: radius, startAngle: Angle(degrees: startAngle), endAngle: endAngle, clockwise: drawnClockwise)
}
}
}
struct Ring: View {
private static let ShadowColor: Color = Color.black.opacity(0.2)
private static let ShadowRadius: CGFloat = 5
private static let ShadowOffsetMultiplier: CGFloat = ShadowRadius + 2
private let ringWidth: CGFloat
private let percent: Double
private let foregroundColor: ColorGradient
private let startAngle: Double = -90
private let touchLocation: CGFloat
private var gradientStartAngle: Double {
self.percent >= 100 ? relativePercentageAngle - 360 : startAngle
}
private var absolutePercentageAngle: Double {
RingShape.percentToAngle(percent: self.percent, startAngle: 0)
}
private var relativePercentageAngle: Double {
// Take into account the startAngle
absolutePercentageAngle + startAngle
}
private var lastGradientColor: Color {
self.foregroundColor.endColor
}
private var ringGradient: AngularGradient {
AngularGradient(
gradient: self.foregroundColor.gradient,
center: .center,
startAngle: Angle(degrees: self.gradientStartAngle),
endAngle: Angle(degrees: relativePercentageAngle)
)
}
init(ringWidth: CGFloat, percent: Double, foregroundColor: ColorGradient, touchLocation:CGFloat) {
self.ringWidth = ringWidth
self.percent = percent
self.foregroundColor = foregroundColor
self.touchLocation = touchLocation
}
var body: some View {
GeometryReader { geometry in
ZStack {
// Background for the ring. Use the final color with reduced opacity
RingShape()
.stroke(style: StrokeStyle(lineWidth: self.ringWidth))
.fill(lastGradientColor.opacity(0.142857))
// Foreground
RingShape(percent: self.percent, startAngle: self.startAngle)
.stroke(style: StrokeStyle(lineWidth: self.ringWidth, lineCap: .round))
.fill(self.ringGradient)
// End of ring with drop shadow
if self.getShowShadow(frame: geometry.size) {
Circle()
.fill(self.lastGradientColor)
.frame(width: self.ringWidth, height: self.ringWidth, alignment: .center)
.offset(x: self.getEndCircleLocation(frame: geometry.size).0,
y: self.getEndCircleLocation(frame: geometry.size).1)
.shadow(color: Ring.ShadowColor,
radius: Ring.ShadowRadius,
x: self.getEndCircleShadowOffset().0,
y: self.getEndCircleShadowOffset().1)
}
}
}
// Padding to ensure that the entire ring fits within the view size allocated
.padding(self.ringWidth / 2)
}
private func getEndCircleLocation(frame: CGSize) -> (CGFloat, CGFloat) {
// Get angle of the end circle with respect to the start angle
let angleOfEndInRadians: Double = relativePercentageAngle.toRadians()
let offsetRadius = min(frame.width, frame.height) / 2
return (offsetRadius * cos(angleOfEndInRadians).toCGFloat(), offsetRadius * sin(angleOfEndInRadians).toCGFloat())
}
private func getEndCircleShadowOffset() -> (CGFloat, CGFloat) {
let angleForOffset = absolutePercentageAngle + (self.startAngle + 90)
let angleForOffsetInRadians = angleForOffset.toRadians()
let relativeXOffset = cos(angleForOffsetInRadians)
let relativeYOffset = sin(angleForOffsetInRadians)
let xOffset = relativeXOffset.toCGFloat() * Ring.ShadowOffsetMultiplier
let yOffset = relativeYOffset.toCGFloat() * Ring.ShadowOffsetMultiplier
return (xOffset, yOffset)
}
private func getShowShadow(frame: CGSize) -> Bool {
if self.percent >= 100 {
return true
}
let circleRadius = min(frame.width, frame.height) / 2
let remainingAngleInRadians = (360 - absolutePercentageAngle).toRadians().toCGFloat()
return circleRadius * remainingAngleInRadians <= self.ringWidth
}
}
struct Ring_Previews: PreviewProvider {
static var previews: some View {
VStack {
Ring(
ringWidth: 50, percent: 5 ,
foregroundColor: ColorGradient(.green, .blue), touchLocation: -1.0
)
.frame(width: 200, height: 200)
Ring(
ringWidth: 20, percent: 110 ,
foregroundColor: ColorGradient(.red, .blue), touchLocation: -1.0
)
.frame(width: 200, height: 200)
}
}
}
@@ -0,0 +1,15 @@
import SwiftUI
public struct RingsChart: ChartBase {
public var chartData = ChartData()
@EnvironmentObject var style: ChartStyle
// TODO - should put background opacity, ring width & spacing as chart style values
public var body: some View {
RingsChartRow(width:10.0, spacing:5.0, chartData: chartData, style: style)
}
public init() {}
}
@@ -0,0 +1,133 @@
//
// RingsChartRow.swift
// ChartViewV2Demo
//
// Created by Dan Wood on 8/20/20.
//
import SwiftUI
public struct RingsChartRow: View {
var width : CGFloat
var spacing : CGFloat
@EnvironmentObject var chartValue: ChartValue
@ObservedObject var chartData: ChartData
@State var touchRadius: CGFloat = -1.0
var style: ChartStyle
public var body: some View {
GeometryReader { geometry in
ZStack {
// FIXME: Why is background circle offset strangely when frame isn't specified? See Preview below. Related to the .animation somehow ????
Circle()
.fill(RadialGradient(gradient: self.style.backgroundColor.gradient, center: .center, startRadius: min(geometry.size.width, geometry.size.height)/2.0, endRadius: 1.0))
ForEach(0..<self.chartData.data.count, id: \.self) { index in
let scaleUp = isRingScaled(size:geometry.size, touchRadius: self.touchRadius, index: index)
let scaledWidth = scaleUp ? self.width * 2.0 : self.width
let normalPadding = (width + spacing) * CGFloat(index)
let scaledDiff = (scaledWidth - width) / 2.0
let padding = min(normalPadding - scaledDiff,
min(geometry.size.width, geometry.size.height)/2.0 - width
// make sure it doesn't get to crazy value
)
Ring(ringWidth:scaledWidth, percent: self.chartData.points[index], foregroundColor:self.style.foregroundColor.rotate(for: index),
touchLocation: self.touchRadius)
.zIndex(scaleUp ? 1 : 0) // make sure zoomed one is on top
.padding(padding)
.animation(Animation.easeIn(duration: 0.5))
}
// .drawingGroup()
}
.gesture(DragGesture()
.onChanged({ value in
let frame = geometry.frame(in: .local)
let radius = min(frame.width, frame.height) / 2.0
let deltaX = value.location.x - frame.midX
let deltaY = value.location.y - frame.midY
self.touchRadius = sqrt(deltaX*deltaX + deltaY*deltaY) // Pythagorean equation
if let currentValue = self.getCurrentValue(maxRadius: radius) {
self.chartValue.currentValue = currentValue
self.chartValue.interactionInProgress = true
}
})
.onEnded({ value in
self.chartValue.interactionInProgress = false
self.touchRadius = -1
})
)
}
}
/// should we scale up the touched ring?
/// - Parameters:
/// - size: size of the view
/// - touchRadius: distance from center where touched
/// - index: which ring is being drawn
/// - Returns: size to scale up or just scale of 1 if not scaled up
func isRingScaled(size: CGSize, touchRadius: CGFloat, index: Int) -> Bool {
let radius = min(size.width, size.height) / 2.0
return index == self.touchedCircleIndex(maxRadius: radius)
}
/// Find which circle has been touched
/// - Parameter maxRadius: radius of overall view circle
/// - Returns: which circle index was touched, if found. 0 = outer, 1 = next one in, etc.
func touchedCircleIndex(maxRadius: CGFloat) -> Int? {
guard self.chartData.data.count > 0 else { return nil } // no data
// Pretend actual circle goes ½ the inter-ring spacing out, so that a touch
// is registered on either side of each ring
let radialDistanceFromEdge = (maxRadius + spacing/2) - self.touchRadius;
guard radialDistanceFromEdge >= 0 else { return nil } // touched outside of ring
let touchIndex = Int(floor(radialDistanceFromEdge / (width + spacing)))
if touchIndex >= self.chartData.data.count { return nil } // too far from outside, no ring
return touchIndex
}
/// Description
/// - Parameter maxRadius: radius of overall view circle
/// - Returns: percentage value of the touched circle, based on `touchRadius` if found
func getCurrentValue(maxRadius: CGFloat) -> Double? {
guard let index = self.touchedCircleIndex(maxRadius: maxRadius) else { return nil }
return self.chartData.points[index]
}
}
struct RingsChartRow_Previews: PreviewProvider {
static var previews: some View {
let multiStyle = ChartStyle(backgroundColor: ColorGradient(Color.black.opacity(0.05), Color.white),
foregroundColor:
[ColorGradient(.purple, .blue),
ColorGradient(.orange, .red),
ColorGradient(.green, .yellow),
])
return RingsChartRow(width:20.0, spacing:10.0, chartData: ChartData([25,50,75,100,125]), style: multiStyle)
// and why does this not get centered when frame isn't specified?
.frame(width:300, height:400)
}
}
@@ -1,30 +0,0 @@
import SwiftUI
extension EnvironmentValues {
var chartType: AnyChartType {
get {
return self[ChartTypeKey.self]
}
set {
self[ChartTypeKey.self] = newValue
}
}
var chartStyle: ChartStyle {
get {
return self[ChartStyleKey.self]
}
set {
self[ChartStyleKey.self] = newValue
}
}
var chartValue: ChartValue {
get {
return self[ChartValueKey.self]
}
set {
self[ChartValueKey.self] = newValue
}
}
}
@@ -1,15 +0,0 @@
import SwiftUI
struct ChartTypeKey: EnvironmentKey {
static let defaultValue: AnyChartType = AnyChartType(BarChart())
}
struct ChartStyleKey: EnvironmentKey {
static let defaultValue: ChartStyle = ChartStyle(backgroundColor: .white,
foregroundColor: ColorGradient(ChartColors.orangeDark,
ChartColors.orangeBright))
}
struct ChartValueKey: EnvironmentKey {
static let defaultValue: ChartValue = ChartValue()
}
@@ -1,10 +1,3 @@
//
// File.swift
//
//
// Created by Nicolas Savoini on 2020-05-25.
//
@testable import SwiftUICharts
import XCTest
+193
View File
@@ -0,0 +1,193 @@
# SwiftUICharts
### Example codes
<p align="left">
<img src="Resources/linechartcard.png" width="400px"/>
</p>
```swift
import SwiftUI
import SwiftUICharts
struct DemoView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Sneakers sold")
.font(.title)
Text("Last week")
.font(.subheadline)
.foregroundColor(.gray)
.padding(.bottom, 8.0)
HStack {
AxisLabels {
ChartGrid {
LineChart()
.showChartMarks(true)
.rangeY(10...40)
.data([12, 34, 23, 18, 36, 22, 26])
.chartStyle(ChartStyle(backgroundColor: .white,
foregroundColor: ColorGradient(.blue, .purple)))
}
.setNumberOfHorizontalLines(5)
.setNumberOfVerticalLines(0)
}
.setAxisXLabels([(1, "M"), (2, "T"), (3, "W"),(4, "T"), (5, "F"), (6, "S"), (7, "S")],
range: 1...7)
.setColor(.gray)
.setFont(.caption2)
VStack(alignment: .leading, spacing: 8.0) {
Text("Highest revenue:")
.font(.callout)
Text("Tuesday")
.font(.subheadline)
.bold()
Text("Most sales:")
.font(.callout)
Text("Friday")
.font(.subheadline)
.bold()
Spacer()
}
.frame(maxWidth: .infinity)
}
}
.padding(16.0)
.background(RoundedRectangle(cornerRadius: 20)
.fill(.white)
.shadow(radius: 8.0))
.padding(32)
.frame(width: 450, height: 350)
}
}
```
<p align="left">
<img src="Resources/barchartcard.png" width="400px"/>
</p>
```swift
import SwiftUI
import SwiftUICharts
struct DemoView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Sneaker brands")
.font(.title)
Text("By popularity")
.font(.subheadline)
.foregroundColor(.gray)
.padding(.bottom, 8.0)
HStack {
AxisLabels {
ChartGrid {
BarChart()
.data([34, 23, 12])
.chartStyle(ChartStyle(backgroundColor: .white,
foregroundColor: [ColorGradient(.red, .orange),
ColorGradient(.blue, .purple),
ColorGradient(.green, .yellow)]))
}
.setNumberOfHorizontalLines(5)
.setNumberOfVerticalLines(0)
}
.setAxisYLabels([(1, "0"), (2, "100"), (3, "200")],
range: 1...3)
.setColor(.gray)
.setFont(.caption2)
VStack(alignment: .leading, spacing: 8.0) {
HStack {
Circle()
.fill(ColorGradient(.red, .orange).linearGradient(from: .bottom, to: .top))
.frame(width: 24.0, height: 24.0)
Text("Sneaker brand 1")
}
HStack {
Circle()
.fill(ColorGradient(.blue, .purple).linearGradient(from: .bottom, to: .top))
.frame(width: 24.0, height: 24.0)
Text("Sneaker brand 2")
}
HStack {
Circle()
.fill(ColorGradient(.green, .yellow).linearGradient(from: .bottom, to: .top))
.frame(width: 24.0, height: 24.0)
Text("Sneaker brand 3")
}
Spacer()
}
.frame(maxWidth: .infinity)
}
}
.padding(16.0)
.background(RoundedRectangle(cornerRadius: 20)
.fill(.white)
.shadow(radius: 8.0))
.padding(32)
.frame(width: 450, height: 350)
}
}
```
<p align="left">
<img src="Resources/piechartcard.png" width="400px"/>
</p>
```swift
import SwiftUI
import SwiftUICharts
struct DemoView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Sneaker brands")
.font(.title)
Text("By popularity")
.font(.subheadline)
.foregroundColor(.gray)
.padding(.bottom, 8.0)
HStack {
PieChart()
.data([34, 23, 12])
.chartStyle(ChartStyle(backgroundColor: .white,
foregroundColor: [ColorGradient(.red, .orange),
ColorGradient(.blue, .purple),
ColorGradient(.yellow, .green),]))
VStack(alignment: .leading, spacing: 8.0) {
HStack {
Circle()
.fill(ColorGradient(.red, .orange).linearGradient(from: .bottom, to: .top))
.frame(width: 24.0, height: 24.0)
Text("Sneaker brand 1")
}
HStack {
Circle()
.fill(ColorGradient(.blue, .purple).linearGradient(from: .bottom, to: .top))
.frame(width: 24.0, height: 24.0)
Text("Sneaker brand 2")
}
HStack {
Circle()
.fill(ColorGradient(.green, .yellow).linearGradient(from: .bottom, to: .top))
.frame(width: 24.0, height: 24.0)
Text("Sneaker brand 3")
}
Spacer()
}
.frame(maxWidth: .infinity)
}
}
.padding(16.0)
.background(RoundedRectangle(cornerRadius: 20)
.fill(.white)
.shadow(radius: 8.0))
.padding(32)
.frame(width: 450, height: 350)
}
}
```