Compare commits

...

3 Commits

19 changed files with 215 additions and 62 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>1</integer>
</dict>
</dict>
</dict>
</plist>
+45 -14
View File
@@ -2,7 +2,7 @@
Swift package for displaying charts effortlessly.
![SwiftUI Charts](./showcase1.gif "SwiftUI Charts")
![SwiftUI Charts](./Resources/showcase1.gif "SwiftUI Charts")
It supports:
* Line charts
@@ -11,9 +11,9 @@ It supports:
### Installation:
It requires iOS 13 and xCode 11!
It requires iOS 13 and Xcode 11!
In xCode got to `File -> Swift Packages -> Add Package Dependency` and paste inthe repo's url: `https://github.com/AppPear/ChartView`
In Xcode got to `File -> Swift Packages -> Add Package Dependency` and paste inthe repo's url: `https://github.com/AppPear/ChartView`
### Usage:
@@ -29,7 +29,7 @@ Added an example project, with **iOS, watchOS** target: https://github.com/AppPe
**New full screen view called LineView!!!**
![Line Charts](./fullscreen2.gif "Line Charts")
![Line Charts](./Resources/fullscreen2.gif "Line Charts")
```swift
LineView(data: [8,23,54,32,12,37,7,23,43], title: "Line chart", legend: "Full screen") // legend is optional, use optional .padding()
@@ -37,7 +37,7 @@ Added an example project, with **iOS, watchOS** target: https://github.com/AppPe
Adopts to dark mode automatically
![Line Charts](./showcase3.gif "Line Charts")
![Line Charts](./Resources/showcase3.gif "Line Charts")
**Line chart is interactive, so you can drag across to reveal the data points**
@@ -51,24 +51,55 @@ You can add a line chart with the following code:
## Bar charts
![Bar Charts](./showcase2.gif "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:
```swift
BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", legend: "Legendary") // legend is optional
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
```
Only points:
```swift
BarChartView(data: ChartData(points: [8,23,54,32,12,37,7,23,43]), title: "Title", legend: "Legendary") // legend is optional
```
**ChartData** structure
Stores values in data pairs (actually tuple): `(String,Double)`
* you can have duplicate values
* keeps the data order
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`
```swift
BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", form: ChartForm.small)
```
```swift
BarChartView(data: ChartData(points: [8,23,54,32,12,37,7,23,43]), title: "Title", form: ChartForm.small)
```
For floating point numbers, you can set a custom specifier:
```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:
@@ -98,9 +129,9 @@ You can access built-in styles:
* barChartMidnightGreenLight
* barChartMidnightGreenDark
![Midnightgreen](./midnightgreen.gif "Midnightgreen")
![Midnightgreen](./Resources/midnightgreen.gif "Midnightgreen")
![Custom Charts](./showcase5.png "Custom Charts")
![Custom Charts](./Resources/showcase5.png "Custom Charts")
### You can customize the size of the chart with a ChartForm object:
@@ -117,10 +148,10 @@ BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", form: ChartForm.s
### WatchOS support for Bar charts:
![Pie Charts](./watchos1.png "Pie Charts")
![Pie Charts](./Resources/watchos1.png "Pie Charts")
## Pie charts
![Pie Charts](./showcase4.png "Pie Charts")
![Pie Charts](./Resources/showcase4.png "Pie Charts")
You can add a line chart with the following code:

Before

Width:  |  Height:  |  Size: 514 KiB

After

Width:  |  Height:  |  Size: 514 KiB

Before

Width:  |  Height:  |  Size: 502 KiB

After

Width:  |  Height:  |  Size: 502 KiB

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

@@ -9,21 +9,21 @@
import SwiftUI
public struct BarChartView : View {
public var data: [Double]
private var data: ChartData
public var title: String
public var legend: String?
public var style: ChartStyle
public var formSize:CGSize
public var dropShadow: Bool
// let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
public var cornerImage: Image
public var valueSpecifier:String
@State private var touchLocation: CGFloat = -1.0
@State private var showValue: Bool = false
@State private var showLabelValue: Bool = false
@State private var currentValue: Double = 0 {
didSet{
if(oldValue != self.currentValue && self.showValue) {
// selectionFeedbackGenerator.selectionChanged()
HapticFeedback.playSelection()
}
}
@@ -31,13 +31,15 @@ public struct BarChartView : View {
var isFullWidth:Bool {
return self.formSize == ChartForm.large
}
public init(data: [Double], title: String, legend: String? = nil, style: ChartStyle = Styles.barChartStyleOrangeLight, form: CGSize? = ChartForm.medium, dropShadow: Bool? = true){
public init(data:ChartData, title: String, legend: String? = nil, style: ChartStyle = Styles.barChartStyleOrangeLight, form: CGSize? = ChartForm.medium, dropShadow: Bool? = true, cornerImage:Image? = Image(systemName: "waveform.path.ecg"), valueSpecifier: String? = "%.1f"){
self.data = data
self.title = title
self.legend = legend
self.style = style
self.formSize = form!
self.dropShadow = dropShadow!
self.cornerImage = cornerImage!
self.valueSpecifier = valueSpecifier!
}
public var body: some View {
@@ -53,7 +55,7 @@ public struct BarChartView : View {
.font(.headline)
.foregroundColor(self.style.textColor)
}else{
Text("\(self.currentValue)")
Text("\(self.currentValue, specifier: self.valueSpecifier)")
.font(.headline)
.foregroundColor(self.style.textColor)
}
@@ -65,16 +67,18 @@ public struct BarChartView : View {
.animation(.easeOut)
}
Spacer()
Image(systemName: "waveform.path.ecg")
self.cornerImage
.imageScale(.large)
.foregroundColor(self.style.legendTextColor)
}.padding()
BarChartRow(data: data, accentColor: self.style.accentColor, secondGradientAccentColor: self.style.secondGradientColor, touchLocation: self.$touchLocation)
if self.legend != nil && self.formSize == ChartForm.medium {
BarChartRow(data: data.points.map{$0.1}, accentColor: self.style.accentColor, secondGradientAccentColor: self.style.secondGradientColor, touchLocation: self.$touchLocation)
if self.legend != nil && self.formSize == ChartForm.medium && !self.showLabelValue{
Text(self.legend!)
.font(.headline)
.foregroundColor(self.style.legendTextColor)
.padding()
}else if (self.data.valuesGiven) {
LabelView(arrowOffset: self.getArrowOffset(touchLocation: self.touchLocation), title: .constant(self.getCurrentValue().0)).offset(x: self.getLabelViewOffset(touchLocation: self.touchLocation), y: -6)
}
}
@@ -83,10 +87,14 @@ public struct BarChartView : View {
.onChanged({ value in
self.touchLocation = value.location.x/self.formSize.width
self.showValue = true
self.currentValue = self.getCurrentValue()
self.currentValue = self.getCurrentValue().1
if(self.data.valuesGiven && self.formSize == ChartForm.medium) {
self.showLabelValue = true
}
})
.onEnded({ value in
self.showValue = false
self.showLabelValue = false
self.touchLocation = -1
})
)
@@ -94,16 +102,31 @@ public struct BarChartView : View {
)
}
func getCurrentValue()-> Double {
let index = max(0,min(self.data.count-1,Int(floor((self.touchLocation*self.formSize.width)/(self.formSize.width/CGFloat(self.data.count))))))
return self.data[index]
func getArrowOffset(touchLocation:CGFloat) -> Binding<CGFloat> {
let realLoc = (self.touchLocation * self.formSize.width) - 50
if realLoc < 10 {
return .constant(realLoc - 10)
}else if realLoc > self.formSize.width-110 {
return .constant((self.formSize.width-110 - realLoc) * -1)
} else {
return .constant(0)
}
}
func getLabelViewOffset(touchLocation:CGFloat) -> CGFloat {
return min(self.formSize.width-110,max(10,(self.touchLocation * self.formSize.width) - 50))
}
func getCurrentValue()-> (String,Double) {
let index = max(0,min(self.data.points.count-1,Int(floor((self.touchLocation*self.formSize.width)/(self.formSize.width/CGFloat(self.data.points.count))))))
return self.data.points[index]
}
}
#if DEBUG
struct ChartView_Previews : PreviewProvider {
static var previews: some View {
BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", legend: "Legendary")
BarChartView(data: TestData.values ,title: "Model 3 sales", legend: "Quarterly", valueSpecifier: "%.0f")
}
}
#endif
@@ -0,0 +1,46 @@
//
// LabelView.swift
// BarChart
//
// Created by Samu András on 2020. 01. 08..
// Copyright © 2020. Samu András. All rights reserved.
//
import SwiftUI
struct LabelView: View {
@Binding var arrowOffset: CGFloat
@Binding var title:String
var body: some View {
VStack{
ArrowUp().fill(Color.white).frame(width: 20, height: 12, alignment: .center).shadow(color: Color.gray, radius: 8, x: 0, y: 0).offset(x: getArrowOffset(offset:self.arrowOffset), y: 12)
ZStack{
RoundedRectangle(cornerRadius: 8).frame(width: 100, height: 32, alignment: .center).foregroundColor(Color.white).shadow(radius: 8)
Text(self.title).font(.caption).bold()
ArrowUp().fill(Color.white).frame(width: 20, height: 12, alignment: .center).zIndex(999).offset(x: getArrowOffset(offset:self.arrowOffset), y: -20)
}
}
}
func getArrowOffset(offset: CGFloat) -> CGFloat {
return max(-36,min(36, offset))
}
}
struct ArrowUp: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: rect.height))
path.addLine(to: CGPoint(x: rect.width/2, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.closeSubpath()
return path
}
}
struct LabelView_Previews: PreviewProvider {
static var previews: some View {
LabelView(arrowOffset: .constant(0), title: .constant("Tesla model 3"))
}
}
+33 -7
View File
@@ -1,6 +1,6 @@
//
// File.swift
//
//
//
// Created by András Samu on 2019. 07. 19..
//
@@ -128,17 +128,43 @@ public struct ChartStyle {
}
}
class ChartData: ObservableObject {
@Published var points: [Double] = [Double]()
@Published var currentPoint: Double? = nil
public class ChartData: ObservableObject {
@Published var points: [(String,Double)] = [(String,Double)]()
var valuesGiven: Bool = false
public init<N: BinaryFloatingPoint>(points:[N]) {
self.points = points.map{("", Double($0))}
}
public init<N: BinaryInteger>(values:[(String,N)]){
self.points = values.map{($0.0, Double($0.1))}
self.valuesGiven = true
}
public init<N: BinaryFloatingPoint>(values:[(String,N)]){
self.points = values.map{($0.0, Double($0.1))}
self.valuesGiven = true
}
public init<N: BinaryInteger>(numberValues:[(N,N)]){
self.points = numberValues.map{(String($0.0), Double($0.1))}
self.valuesGiven = true
}
public init<N: BinaryFloatingPoint & LosslessStringConvertible>(numberValues:[(N,N)]){
self.points = numberValues.map{(String($0.0), Double($0.1))}
self.valuesGiven = true
}
init(points:[Double]) {
self.points = points
public func onlyPoints() -> [Double] {
return self.points.map{ $0.1 }
}
}
class TestData{
public class TestData{
static public var data:ChartData = ChartData(points: [37,72,51,22,39,47,66,85,50])
static public var values:ChartData = ChartData(values: [("2017 Q3",220),
("2017 Q4",1550),
("2018 Q1",8180),
("2018 Q2",18440),
("2018 Q3",55840),
("2018 Q4",63150), ("2019 Q1",50900), ("2019 Q2",77550), ("2019 Q3",79600), ("2019 Q4",92550)])
}
extension Color {
+7 -4
View File
@@ -22,7 +22,8 @@ struct Legend: View {
return frame.size.width / CGFloat(data.points.count-1)
}
var stepHeight: CGFloat {
if let min = data.points.min(), let max = data.points.max(), min != max {
let points = self.data.onlyPoints()
if let min = points.min(), let max = points.max(), min != max {
if (min < 0){
return (frame.size.height-padding) / CGFloat(max - min)
}else{
@@ -33,7 +34,8 @@ struct Legend: View {
}
var min: CGFloat {
return CGFloat(data.points.min() ?? 0)
let points = self.data.onlyPoints()
return CGFloat(points.min() ?? 0)
}
var body: some View {
@@ -80,8 +82,9 @@ struct Legend: View {
}
func getYLegend() -> [Double]? {
guard let max = data.points.max() else { return nil }
guard let min = data.points.min() else { return nil }
let points = self.data.onlyPoints()
guard let max = points.max() else { return nil }
guard let min = points.min() else { return nil }
let step = Double(max - min)/4
return [min+step * 0, min+step * 1, min+step * 2, min+step * 3, min+step * 4]
}
+8 -5
View File
@@ -23,20 +23,23 @@ struct Line: View {
return frame.size.width / CGFloat(data.points.count-1)
}
var stepHeight: CGFloat {
if let min = data.points.min(), let max = data.points.max(), min != max {
let points = self.data.onlyPoints()
if let min = points.min(), let max = points.max(), min != max {
if (min <= 0){
return (frame.size.height-padding) / CGFloat(data.points.max()! - data.points.min()!)
return (frame.size.height-padding) / CGFloat(points.max()! - points.min()!)
}else{
return (frame.size.height-padding) / CGFloat(data.points.max()! + data.points.min()!)
return (frame.size.height-padding) / CGFloat(points.max()! + points.min()!)
}
}
return 0
}
var path: Path {
return Path.quadCurvedPathWithPoints(points: data.points, step: CGPoint(x: stepWidth, y: stepHeight))
let points = self.data.onlyPoints()
return Path.quadCurvedPathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight))
}
var closedPath: Path {
return Path.quadClosedCurvedPathWithPoints(points: data.points, step: CGPoint(x: stepWidth, y: stepHeight))
let points = self.data.onlyPoints()
return Path.quadClosedCurvedPathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight))
}
var body: some View {
@@ -16,6 +16,7 @@ public struct LineChartView: View {
public var style: ChartStyle
public var formSize:CGSize
public var dropShadow: Bool
public var valueSpecifier:String
@State private var touchLocation:CGPoint = .zero
@State private var showIndicatorDot: Bool = false
@@ -31,7 +32,7 @@ public struct LineChartView: View {
let frame = CGSize(width: 180, height: 120)
private var rateValue: Int
public init(data: [Double], title: String, legend: String? = nil, style: ChartStyle = Styles.lineChartStyleOne, form: CGSize? = ChartForm.medium ,rateValue: Int? = 14, dropShadow: Bool? = true){
public init(data: [Double], title: String, legend: String? = nil, style: ChartStyle = Styles.lineChartStyleOne, form: CGSize? = ChartForm.medium ,rateValue: Int? = 14, dropShadow: Bool? = true, valueSpecifier: String? = "%.1f"){
self.data = ChartData(points: data)
self.title = title
self.legend = legend
@@ -39,6 +40,7 @@ public struct LineChartView: View {
self.formSize = form!
self.rateValue = rateValue!
self.dropShadow = dropShadow!
self.valueSpecifier = valueSpecifier!
}
public var body: some View {
@@ -66,7 +68,7 @@ public struct LineChartView: View {
}else{
HStack{
Spacer()
Text("\(self.currentValue, specifier: "%.2f")")
Text("\(self.currentValue, specifier: self.valueSpecifier)")
.font(.system(size: 41, weight: .bold, design: .default))
.offset(x: 0, y: 30)
Spacer()
@@ -97,13 +99,14 @@ public struct LineChartView: View {
}
@discardableResult func getClosestDataPoint(toPoint: CGPoint, width:CGFloat, height: CGFloat) -> CGPoint {
let stepWidth: CGFloat = width / CGFloat(data.points.count-1)
let stepHeight: CGFloat = height / CGFloat(data.points.max()! + data.points.min()!)
let points = self.data.onlyPoints()
let stepWidth: CGFloat = width / CGFloat(points.count-1)
let stepHeight: CGFloat = height / CGFloat(points.max()! + points.min()!)
let index:Int = Int(round((toPoint.x)/stepWidth))
if (index >= 0 && index < data.points.count){
self.currentValue = self.data.points[index]
return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(self.data.points[index])*stepHeight)
if (index >= 0 && index < points.count){
self.currentValue = points[index]
return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(points[index])*stepHeight)
}
return .zero
}
+15 -11
View File
@@ -13,7 +13,8 @@ public struct LineView: View {
public var title: String?
public var legend: String?
public var style: ChartStyle
public var valueSpecifier:String
@Environment(\.colorScheme) var colorScheme: ColorScheme
@State private var showLegend = false
@State private var dragLocation:CGPoint = .zero
@@ -23,11 +24,12 @@ public struct LineView: View {
@State private var currentDataNumber: Double = 0
@State private var hideHorizontalLines: Bool = false
public init(data: [Double], title: String? = nil, legend: String? = nil, style: ChartStyle? = Styles.lineChartStyleOne){
public init(data: [Double], title: String? = nil, legend: String? = nil, style: ChartStyle? = Styles.lineChartStyleOne, valueSpecifier: String? = "%.1f"){
self.data = ChartData(points: data)
self.title = title
self.legend = legend
self.style = style!
self.valueSpecifier = valueSpecifier!
}
public var body: some View {
@@ -54,7 +56,7 @@ public struct LineView: View {
self.showLegend.toggle()
}
}.frame(width: geometry.frame(in: .local).size.width, height: 240).offset(x: 0, y: 40 )
MagnifierRect(currentNumber: self.$currentDataNumber)
MagnifierRect(currentNumber: self.$currentDataNumber, valueSpecifier: self.valueSpecifier)
.opacity(self.opacity)
.offset(x: self.dragLocation.x - geometry.frame(in: .local).size.width/2, y: 36)
}
@@ -77,13 +79,14 @@ public struct LineView: View {
}
func getClosestDataPoint(toPoint: CGPoint, width:CGFloat, height: CGFloat) -> CGPoint {
let stepWidth: CGFloat = width / CGFloat(data.points.count-1)
let stepHeight: CGFloat = height / CGFloat(data.points.max()! + data.points.min()!)
let points = self.data.onlyPoints()
let stepWidth: CGFloat = width / CGFloat(points.count-1)
let stepHeight: CGFloat = height / CGFloat(points.max()! + points.min()!)
let index:Int = Int(floor((toPoint.x-15)/stepWidth))
if (index >= 0 && index < data.points.count){
self.currentDataNumber = self.data.points[index]
return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(self.data.points[index])*stepHeight)
if (index >= 0 && index < points.count){
self.currentDataNumber = points[index]
return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(points[index])*stepHeight)
}
return .zero
}
@@ -105,13 +108,14 @@ struct IndicatorCircle: View {
struct MagnifierRect: View {
@Binding var currentNumber: Double
var valueSpecifier:String
@Environment(\.colorScheme) var colorScheme: ColorScheme
var body: some View {
ZStack{
Text("\(self.currentNumber, specifier: "%.2f")")
Text("\(self.currentNumber, specifier: valueSpecifier)")
.font(.system(size: 18, weight: .bold))
.offset(x: 0, y:-110)
.animation(.spring())
// .animation(.spring())
.foregroundColor(self.colorScheme == .dark ? Color.white : Color.black)
if (self.colorScheme == .dark ){
RoundedRectangle(cornerRadius: 16)
@@ -127,6 +131,6 @@ struct MagnifierRect: View {
}
}.animation(.linear)
}//.animation(.linear)
}
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 445 KiB