Compare commits

...

67 Commits

Author SHA1 Message Date
Andras Samu 7853476d86 Add CardView and CardLabel 2020-05-30 18:00:18 +02:00
nicolas d64d0e9d7a Bug Fix: Bar Chart with [0] crashed (#110) 2020-05-30 17:18:01 +02:00
Andras Samu 0caebce9ff added mac os as a build target 2020-05-28 18:50:59 +02:00
nicolas f2866ae281 Syntax corrections (#105)
* Movie PieChartFile

* Variable renaming
2020-05-28 10:54:06 +02:00
Adrian 4963ec54ed Write unit tests for Color+Extension.swift (#101)
- Wrote tests
- Possible minor bug where there's an alpha missing on 32-bit colors?
2020-05-26 23:05:05 +02:00
nicolas 99b952fcf4 Restore rotating index for multicolor (#102) 2020-05-26 23:04:51 +02:00
Andras Samu b5beab55e6 removed .rotate for foreGroundColor 2020-05-25 14:50:01 +02:00
nicolas aa9126482f Add PieChart + multicolor (#98)
* Add ColorGradient example
Add ColorGradient single color constructor
Add preview for BarChart

* Add PieChart
Allow multi color for Pie and Bar
Add linter
2020-05-25 14:20:46 +02:00
Adrian a2d75dca0e Write unit tests for CGPoint+Extension.swift (#100)
- write unit tests for CGPoint+Extension.swift
- clean up formatting on CGPoint+Extension.swift
2020-05-25 09:36:47 +02:00
Andras Samu 5cd5858967 Added a first implementation of BarChart and LineChart also introduced style 2020-05-24 18:38:19 +02:00
Andras Samu d869e4186f Replace the old with a more sleek and flexible code architecture 2020-05-22 17:42:35 +02:00
Daniel dd7a1fc9bd Line view custom gradient (#67)
* feat: Added gradient to Line init from LineView

* Making GradientColor's init method as public
2020-05-18 12:23:21 +02:00
josephwalden13 fada162030 change rateValue to optional to fix crash from force unwraps and set it to show in the chart only for non zero values (#71) 2020-05-07 11:55:35 +02:00
Pierluigi Dell'Acqua a242bd3c94 Making GradientColor's init method as public (#63)
Making the method as public, application is allowed to define custom GradientColor.
2020-04-26 16:22:29 +02:00
Andras Samu c12c773af0 fixed unwrap error 2020-03-17 10:38:45 +01:00
Andras Samu 75804f470a Customisable drop shadow color (#53)
* Customisable drop shadow color

* Drop shadow color parameter in ChartStyle

* Fixed BarChartView
2020-03-05 11:57:38 +01:00
Andras Samu 04989ad159 Updated ReadMe with multilinechartview 2020-03-04 14:21:42 +01:00
Andras Samu 7365bc91ef Added MultiLineChartView, fixed LineView legend disappearing on navigating back 2020-03-04 14:03:34 +01:00
Andras Samu 257e5fca30 Fixed global max and min for multiline chartview 2020-03-03 19:10:10 +01:00
Fredrik Lillejordet 6a9546bb1f added id: self in ForEach for dynamic content in BarChartRow (#49)
Co-authored-by: Andras Samu <samu.andris1@gmail.com>
2020-03-03 12:45:45 +01:00
Andrew Yang b230ed0369 Fix to use dark mode settings for barchatview label text. (#47) 2020-03-03 12:42:30 +01:00
Andras Samu a8b4101c52 Merge remote-tracking branch 'refs/remotes/origin/master' 2020-03-03 12:41:42 +01:00
Andras Samu 2a1b55f79f Adding multiline chartview and straight linechart 2020-03-03 12:41:11 +01:00
Andras Samu 47731bfeff Added cutom darkmode style description 2020-02-13 12:21:20 +01:00
Andras Samu 841bde1377 Fixed infinite size compile error 2020-02-13 12:15:28 +01:00
Andras Samu 37779e1b54 added a darkmodestyle so you can customize darkmode appearance for lineview, linechartview, barchartview 2020-02-13 11:59:21 +01:00
Andras Samu ba5bc4f861 Fixed barchart crashing for empty array 2020-02-13 11:31:49 +01:00
Andras Samu 80d546de03 fixed lineview for small negative numbers 2020-02-13 11:20:39 +01:00
xspyhack 88db9aeafe Fix line chart view indicator point (#40) 2020-01-22 13:56:03 +01:00
Kevin Fowler 37c51d9b46 Fix llvm segfault when archiving SwiftUICharts (#36)
I added ChartView/SwiftUICharts to my iphone project. It worked as
expected until I tried to archive the application for distribution.

In the archive step, compilation fails with a segmentation fault:
    1.	While running pass #48703 SILFunctionTransform "GenericSpecializer" on SILFunction "@$s13SwiftUICharts9ChartDataC6pointsACSayxG_tcSBRzlufcSd_Tg5".
     for 'init(points:)' (at /Users/kfowler/projects/ChartView/Sources/SwiftUICharts/Helpers.swift:134:12)
    0  swift                    0x0000000113b94a63 PrintStackTraceSignalHandler(void*) + 51
    1  swift                    0x0000000113b94236 SignalHandler(int) + 358
    2  libsystem_platform.dylib 0x00007fff6bd7d42d _sigtramp + 29
    3  libsystem_platform.dylib 0x0000000800000001 _sigtramp + 2485660657
    4  swift                    0x000000010ffd800c swift::ReabstractionInfo::prepareAndCheck(swift::ApplySite, swift::SILFunction*, swift::SubstitutionMap, swift::OptRemark::Emitter*) + 572
    5  swift                    0x000000011002abda swift::ReabstractionInfo::ReabstractionInfo(swift::ApplySite, swift::SILFunction*, swift::SubstitutionMap, swift::IsSerialized_t, bool, swift::OptRemark::Emitter*) + 122
    6  swift                    0x0000000110034c35 swift::trySpecializeApplyOfGeneric(swift::SILOptFunctionBuilder&, swift::ApplySite, llvm::SmallSetVector<swift::SILInstruction*, 8u>&, llvm::SmallVectorImpl<swift::SILFunction*>&, swift::OptRemark::Emitter&) + 1653
    7  swift                    0x000000010ff0d731 (anonymous namespace)::GenericSpecializer::run() + 2673
    8  swift                    0x000000010fe86f3e swift::SILPassManager::execute() + 4606
    9  swift                    0x000000010fae596b swift::CompilerInstance::performSILProcessing(swift::SILModule*, swift::UnifiedStatsReporter*) + 6379
    10 swift                    0x000000010f7ddec5 performCompile(swift::CompilerInstance&, swift::CompilerInvocation&, llvm::ArrayRef<char const*>, int&, swift::FrontendObserver*, swift::UnifiedStatsReporter*) + 33925
    11 swift                    0x000000010f7d2234 swift::performFrontend(llvm::ArrayRef<char const*>, char const*, void*, swift::FrontendObserver*) + 6820
    12 swift                    0x000000010f75f733 main + 1219
    13 libdyld.dylib            0x00007fff6bb847fd start + 1
error: Segmentation fault: 11 (in target 'SwiftUICharts' from project 'SwiftUICharts')

This change resolves the issue.
2020-01-17 12:29:47 +01:00
Daniel Barclay 75df39fc1f Fix animation undoing itself by multiple calls of toggle() (#35) 2020-01-17 12:29:03 +01:00
Leanne 6b5affa46e Update README.md (#33) 2020-01-16 10:47:00 +01:00
Andras Samu 1e362b9eea Added Label,Value pairs so you can display a label for each point in Bar chart, added ability to change ecg image in the corner, added Generic number types to ChartData initialiser 2020-01-11 12:36:42 +01:00
Andras Samu f7d9895e36 Merge remote-tracking branch 'refs/remotes/origin/master' 2020-01-07 09:57:32 +01:00
Andras Samu 04b6e385ea Fixed chart clipping, and value animation issue 2020-01-07 09:56:16 +01:00
Tieda 9f2e3d32df Fixed Xcode typo in README (#32) 2020-01-05 11:17:19 +01:00
Ricky Cai 524aec2a04 Fixed Issue 28 (#29)
Changed Int to Double in quadCurvedPathWithPoints and quadClosedCurvedPathWithPoints.
2019-12-28 09:00:48 +01:00
Andras Samu 03f90728b4 solved line view negative numbers, also when it crashed 0 or 1 element data set 2019-12-27 21:37:28 +01:00
Andras Samu 0d95dbd3d4 Fixed: Form redeclaration as ChartForm issue #23 2019-12-12 14:31:07 +01:00
Steven Zweier fd14ca2327 Allow graphs to accept Double (#19) 2019-11-24 21:20:47 +01:00
Andras Samu 20fb782a3e quick fix for 0 elements in line view 2019-11-13 10:16:39 +01:00
Andras Samu 9fa7e20221 Merge remote-tracking branch 'refs/remotes/origin/master' 2019-11-13 10:00:49 +01:00
Andras Samu b5e3aa897c added self 2019-11-13 09:48:40 +01:00
Andras Samu 1b45a6a922 Update README.md 2019-11-11 21:53:57 +01:00
Andras Samu d3d0b086f1 updated readme 2019-11-11 21:50:33 +01:00
Andras Samu 6cc43d9dc0 fixed legend in dark mode 2019-11-11 21:20:13 +01:00
Andras Samu e081d3a88d fixed loupe 2019-11-11 21:13:10 +01:00
Andras Samu e5c309eac4 set to public 2019-11-11 20:41:42 +01:00
Andras Samu 3f542f92e3 Added LineView 2019-11-11 20:29:41 +01:00
sy1995 7a9f013631 Hi, I find that the LineChartView has a value is static (#15)
* add rateValue to LineChart

* modify

* modify rate value to linechart

* modify

* modify
2019-11-11 15:58:21 +01:00
Andras Samu 8dffe79e28 Merge pull request #10 from jufabeck2202/Uint64
'scanHexInt32' was deprecated in iOS 13.0
2019-10-15 09:40:19 +02:00
Julian Beck d5a1c0065c 'scanHexInt32' was deprecated in iOS 13.0 2019-10-10 21:52:21 +02:00
Andras Samu b9761d3eb9 Update README.md 2019-10-07 10:51:25 +02:00
Andras Samu c3c0a8a7c4 Merge pull request #8 from WayneEld/issue/print-line-removed
Removal of index print line.
2019-10-04 11:46:05 +02:00
Wayne Eldridge b597faac76 Removal of index print line. 2019-10-03 20:02:42 +02:00
Andras Samu fc1099f486 Update README.md 2019-09-30 20:45:32 +02:00
Andras Samu 94950db4d5 added watchos image 2019-09-30 20:42:30 +02:00
Andras Samu 251a830281 Added watchOS support 2019-09-30 20:41:04 +02:00
Andras Samu b2b0b83b4b fixed style error 2019-09-30 10:31:34 +02:00
Andras Samu 94a98ee2c7 fixed public property 2019-09-30 10:28:01 +02:00
Andras Samu da015797dd fixed midnight green shade 2019-09-19 15:54:10 +02:00
Andras Samu 617297cbcd updated color preset names 2019-09-19 15:48:35 +02:00
Andras Samu 3909818a3f Add files via upload 2019-09-19 15:41:06 +02:00
Andras Samu df24b3d1c2 Update README.md 2019-09-19 15:40:00 +02:00
Andras Samu 07a16d5548 Update README.md 2019-09-19 15:39:20 +02:00
Andras Samu 4512fd0d3e Added midnight green color styles 2019-09-19 14:56:03 +02:00
Andras Samu 8a54155e47 Added public modifier 2019-09-11 22:08:33 +02:00
57 changed files with 1606 additions and 866 deletions
+64
View File
@@ -0,0 +1,64 @@
disabled_rules:
- explicit_acl
- trailing_whitespace
- force_cast
- unused_closure_parameter
- multiple_closures_with_trailing_closure
opt_in_rules:
- anyobject_protocol
- array_init
- attributes
- collection_alignment
- colon
- conditional_returns_on_newline
- convenience_type
- empty_count
- empty_string
- empty_collection_literal
- enum_case_associated_values_count
- function_default_parameter_at_end
- fatal_error_message
- file_name
- first_where
- modifier_order
- toggle_bool
- unused_private_declaration
- yoda_condition
excluded:
- Carthage
- Pods
- SwiftLint/Common/3rdPartyLib
identifier_name:
excluded:
- a
- b
- c
- i
- id
- t
- to
- x
- y
line_length:
warning: 150
error: 200
ignores_function_declarations: true
ignores_comments: true
ignores_urls: true
function_body_length:
warning: 300
error: 500
function_parameter_count:
warning: 6
error: 8
type_body_length:
warning: 300
error: 400
file_length:
warning: 500
error: 1200
ignore_comment_only_lines: true
cyclomatic_complexity:
warning: 15
error: 21
reporter: "xcode"
@@ -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>
+3 -3
View File
@@ -6,13 +6,13 @@ import PackageDescription
let package = Package(
name: "SwiftUICharts",
platforms: [
.iOS(.v13),
.iOS(.v13), .watchOS(.v6), .macOS(.v10_15)
],
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
.library(
name: "SwiftUICharts",
targets: ["SwiftUICharts"]),
targets: ["SwiftUICharts"])
],
dependencies: [
// Dependencies declare other packages that this package depends on.
@@ -26,6 +26,6 @@ let package = Package(
dependencies: []),
.testTarget(
name: "SwiftUIChartsTests",
dependencies: ["SwiftUICharts"]),
dependencies: ["SwiftUICharts"])
]
)
+128 -19
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:
@@ -21,8 +21,52 @@ import the package in the file you would like to use it: `import SwiftUICharts`
You can display a Chart by adding a chart view to your parent view:
### Demo
Added an example project, with **iOS, watchOS** target: https://github.com/AppPear/ChartViewDemo
## Line charts
![Line Charts](./showcase3.gif "Line Charts")
**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")
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)`
Available preset gradients:
* orange
* blue
* green
* blu
* bluPurpl
* purple
* prplPink
* prplNeon
* orngPink
**Full screen view called LineView!!!**
![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()
```
Adopts to dark mode automatically
![Line Charts](./Resources/showcase3.gif "Line Charts")
You can add your custom darkmode style by specifying:
```swift
let myCustomStyle = ChartStyle(...)
let myCutsomDarkModeStyle = ChartStyle(...)
myCustomStyle.darkModeStyle = myCutsomDarkModeStyle
```
**Line chart is interactive, so you can drag across to reveal the data points**
@@ -32,52 +76,117 @@ You can add a line chart with the following code:
LineChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", legend: "Legendary") // legend is optional
```
**Turn drop shadow off by adding to the Initialiser: `dropShadow: false`**
## 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
```
You can add different formats:
* Small `Form.small`
* Medium `Form.medium`
* Large `Form.large`
**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: 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`**
```swift
BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", style: ChartStyle(formSize: Form.small))
```
### You can customize styling of the chart with a ChartStyle object:
Customizable:
* background color
* accent color
* second gradient color
* chart form size
* text color
* legend text color
```swift
let chartStyle = ChartStyle(backgroundColor: Color.black, accentColor: Colors.OrangeStart, secondGradientColor: Colors.OrangeEnd, chartFormSize: Form.medium, textColor: Color.white, legendTextColor: Color.white )
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)
```
![Custom Charts](./showcase5.png "Custom Charts")
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](./showcase4.png "Pie Charts")
![Pie Charts](./Resources/showcase4.png "Pie Charts")
You can add a line chart with the following code:
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: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

@@ -1,49 +0,0 @@
//
// ChartCell.swift
// ChartView
//
// Created by András Samu on 2019. 06. 12..
// Copyright © 2019. András Samu. All rights reserved.
//
import SwiftUI
public struct BarChartCell : View {
var value: Double
var index: Int = 0
var width: Float
var numberOfDataPoints: Int
var cellWidth: Double {
return Double(width)/(Double(numberOfDataPoints) * 1.5)
}
var accentColor: Color
var secondGradientAccentColor: Color?
var gradientColors:[Color] {
if (secondGradientAccentColor != nil) {
return [secondGradientAccentColor!, accentColor]
}
return [accentColor, accentColor]
}
@State var scaleValue: Double = 0
@Binding var touchLocation: CGFloat
public var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 4)
.fill(LinearGradient(gradient: Gradient(colors: gradientColors), startPoint: .bottom, endPoint: .top))
}
.frame(width: CGFloat(self.cellWidth))
.scaleEffect(CGSize(width: 1, height: self.scaleValue), anchor: .bottom)
.onAppear(){
self.scaleValue = self.value
}
.animation(Animation.spring().delay(self.touchLocation < 0 ? Double(self.index) * 0.04 : 0))
}
}
#if DEBUG
struct ChartCell_Previews : PreviewProvider {
static var previews: some View {
BarChartCell(value: Double(0.75), width: 320, numberOfDataPoints: 12, accentColor: Colors.OrangeStart, secondGradientAccentColor: nil, touchLocation: .constant(-1))
}
}
#endif
@@ -1,39 +0,0 @@
//
// ChartRow.swift
// ChartView
//
// Created by András Samu on 2019. 06. 12..
// Copyright © 2019. András Samu. All rights reserved.
//
import SwiftUI
public struct BarChartRow : View {
var data: [Int]
var accentColor: Color
var secondGradientAccentColor: Color?
var maxValue: Int {
data.max() ?? 0
}
@Binding var touchLocation: CGFloat
public var body: some View {
GeometryReader { geometry in
HStack(alignment: .bottom, spacing: (geometry.frame(in: .local).width-22)/CGFloat(self.data.count * 3)){
ForEach(0..<self.data.count) { i in
BarChartCell(value: Double(self.data[i])/Double(self.maxValue), index: i, width: Float(geometry.frame(in: .local).width - 22), numberOfDataPoints: self.data.count, accentColor: self.accentColor, secondGradientAccentColor: self.secondGradientAccentColor, touchLocation: self.$touchLocation)
.scaleEffect(self.touchLocation > CGFloat(i)/CGFloat(self.data.count) && self.touchLocation < CGFloat(i+1)/CGFloat(self.data.count) ? CGSize(width: 1.4, height: 1.1) : CGSize(width: 1, height: 1), anchor: .bottom)
}
}
.padding([.top, .leading, .trailing], 10)
}
}
}
#if DEBUG
struct ChartRow_Previews : PreviewProvider {
static var previews: some View {
BarChartRow(data: [8,23,54,32,12,37,7], accentColor: Colors.OrangeStart, touchLocation: .constant(-1))
}
}
#endif
@@ -1,104 +0,0 @@
//
// ChartView.swift
// ChartView
//
// Created by András Samu on 2019. 06. 12..
// Copyright © 2019. András Samu. All rights reserved.
//
import SwiftUI
public struct BarChartView : View {
public var data: [Int]
public var title: String
public var legend: String?
public var style: ChartStyle
let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
@State private var touchLocation: CGFloat = -1.0
@State private var showValue: Bool = false
@State private var currentValue: Int = 0 {
didSet{
if(oldValue != self.currentValue && self.showValue) {
selectionFeedbackGenerator.selectionChanged()
}
}
}
var isFullWidth:Bool {
return self.style.chartFormSize == Form.large
}
public init(data: [Int], title: String, legend: String? = nil, style: ChartStyle = Styles.barChartStyleOne ){
self.data = data
self.title = title
self.legend = legend
self.style = style
}
public var body: some View {
ZStack{
Rectangle()
.fill(self.style.backgroundColor)
.cornerRadius(20)
.shadow(color: Color.gray, radius: 8 )
VStack(alignment: .leading){
HStack{
if(!showValue){
Text(self.title)
.font(.headline)
.foregroundColor(self.style.textColor)
}else{
Text("\(self.currentValue)")
.font(.headline)
.foregroundColor(self.style.textColor)
}
if(self.style.chartFormSize == Form.large && self.legend != nil && !showValue) {
Text(self.legend!)
.font(.callout)
.foregroundColor(self.style.accentColor)
.transition(.opacity)
.animation(.easeOut)
}
Spacer()
Image(systemName: "waveform.path.ecg")
.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.style.chartFormSize == Form.medium {
Text(self.legend!)
.font(.headline)
.foregroundColor(self.style.legendTextColor)
.padding()
}
}
}.frame(minWidth:self.style.chartFormSize.width, maxWidth: self.isFullWidth ? .infinity : self.style.chartFormSize.width, minHeight:self.style.chartFormSize.height, maxHeight:self.style.chartFormSize.height)
.gesture(DragGesture()
.onChanged({ value in
self.touchLocation = value.location.x/self.style.chartFormSize.width
self.showValue = true
self.currentValue = self.getCurrentValue()
})
.onEnded({ value in
self.showValue = false
self.touchLocation = -1
})
)
.gesture(TapGesture()
)
}
func getCurrentValue()-> Int{
let index = max(0,min(self.data.count-1,Int(floor((self.touchLocation*self.style.chartFormSize.width)/(self.style.chartFormSize.width/CGFloat(self.data.count))))))
print(index)
return self.data[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")
}
}
#endif
@@ -0,0 +1,23 @@
import SwiftUI
public struct CardView<Content: View>: View {
@Environment(\.chartStyle) private var chartStyle
let content: () -> Content
public init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
public var body: some View {
ZStack{
Rectangle()
.fill(self.chartStyle.backgroundColor.linearGradient(from: .bottom, to: .top))
.cornerRadius(20)
.shadow(color: Color.gray, radius: 8)
VStack {
self.content()
}
}
}
}
@@ -0,0 +1,19 @@
import SwiftUI
struct AnyChartType: ChartType {
private let chartMaker: (ChartType.Configuration, ChartType.Style) -> AnyView
init<S: ChartType>(_ type: S) {
self.chartMaker = type.makeTypeErasedBody
}
func makeChart(configuration: ChartType.Configuration, style: ChartType.Style) -> AnyView {
self.chartMaker(configuration, style)
}
}
fileprivate extension ChartType {
func makeTypeErasedBody(configuration: ChartType.Configuration, style: ChartType.Style) -> AnyView {
AnyView(makeChart(configuration: configuration, style: style))
}
}
@@ -0,0 +1,11 @@
import SwiftUI
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ChartType {
associatedtype Body: View
func makeChart(configuration: Self.Configuration, style: Self.Style) -> Self.Body
typealias Configuration = ChartTypeConfiguration
typealias Style = ChartStyle
}
@@ -0,0 +1,5 @@
import SwiftUI
public struct ChartTypeConfiguration {
public let data: [Double]
}
@@ -0,0 +1,21 @@
// ChartView.swift
// Created by Samu András on 2020. 05. 22..
import SwiftUI
public struct ChartView: View {
@Environment(\.chartType) private var chartType
@Environment(\.chartStyle) private var chartStyle
private var configuration: ChartTypeConfiguration
public var body: some View {
self.chartType.makeChart(configuration: configuration, style: chartStyle)
}
}
extension ChartView {
public init(data: [Double]) {
self.configuration = ChartTypeConfiguration(data: data)
}
}
@@ -0,0 +1,22 @@
//
// File.swift
//
//
// Created by Nicolas Savoini on 2020-05-25.
//
import Foundation
extension Array where Element == ColorGradient {
func rotate(for index: Int) -> ColorGradient {
if self.isEmpty {
return ColorGradient.orangeBright
}
if self.count <= index {
return self[index % self.count]
}
return self[index]
}
}
@@ -0,0 +1,35 @@
import SwiftUI
extension CGPoint {
static func getStep(frame: CGRect, data: [Double]) -> CGPoint {
let padding: CGFloat = 30.0
// stepWidth
var stepWidth: CGFloat = 0.0
if data.count < 2 {
stepWidth = 0.0
}
stepWidth = frame.size.width / CGFloat(data.count - 1)
// stepHeight
var stepHeight: CGFloat = 0.0
var min: Double?
var max: Double?
if let minPoint = data.min(), let maxPoint = data.max(), minPoint != maxPoint {
min = minPoint
max = maxPoint
} else {
return .zero
}
if let min = min, let max = max, min != max {
if min <= 0 {
stepHeight = (frame.size.height - padding) / CGFloat(max - min)
} else {
stepHeight = (frame.size.height - padding) / CGFloat(max + min)
}
}
return CGPoint(x: stepWidth, y: stepHeight)
}
}
@@ -0,0 +1,16 @@
//
// 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
public var mid: CGPoint {
return CGPoint(x: self.midX, y: self.midY)
}
}
@@ -0,0 +1,23 @@
import SwiftUI
extension Color {
init(hexString: String) {
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int = UInt64()
Scanner(string: hex).scanHexInt64(&int)
let red, green, blue: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(red, green, blue) = ((int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(red, green, blue) = (int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
// FIXME: I think we need an an alpha value on this one. See link below.
// https://stackoverflow.com/a/56874327/4475605
(red, green, blue) = (int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(red, green, blue) = (0, 0, 0)
}
self.init(red: Double(red) / 255, green: Double(green) / 255, blue: Double(blue) / 255)
}
}
@@ -0,0 +1,351 @@
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)
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?
var point = CGPoint.zero
forEach { ele in
switch ele {
case .move(let to):
if start == nil {
start = to
}
point = to
case .line(let to):
ret += point.line(to: to)
point = to
case .quadCurve(let to, let control):
ret += point.quadCurve(to: to, control: control)
point = to
case .curve(let to, let control1, let control2):
ret += point.curve(to: to, control1: control1, control2: control2)
point = to
case .closeSubpath:
if let to = start {
ret += point.line(to: to)
point = to
}
start = nil
}
}
return ret
}
func length(to maxX: CGFloat) -> CGFloat {
var ret: CGFloat = 0.0
var start: CGPoint?
var point = CGPoint.zero
var finished = false
forEach { ele in
if finished {
return
}
switch ele {
case .move(let to):
if to.x > maxX {
finished = true
return
}
if start == nil {
start = to
}
point = to
case .line(let to):
if to.x > maxX {
finished = true
ret += point.line(to: to, x: maxX)
return
}
ret += point.line(to: to)
point = to
case .quadCurve(let to, let control):
if to.x > maxX {
finished = true
ret += point.quadCurve(to: to, control: control, x: maxX)
return
}
ret += point.quadCurve(to: to, control: control)
point = to
case .curve(let to, let control1, let control2):
if to.x > maxX {
finished = true
ret += point.curve(to: to, control1: control1, control2: control2, x: maxX)
return
}
ret += point.curve(to: to, control1: control1, control2: control2)
point = to
case .closeSubpath:
fatalError("Can't include closeSubpath")
}
}
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 }
var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
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))
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 quadClosedCurvedPathWithPoints(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 }
path.move(to: .zero)
var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
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))
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
}
path.addLine(to: CGPoint(x: point1.x, y: 0))
path.closeSubpath()
return path
}
static func linePathWithPoints(points: [Double], step: CGPoint) -> Path {
var path = Path()
if points.count < 2 {
return path
}
guard let offset = points.min() else {
return path
}
let point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
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))
path.addLine(to: point2)
}
return path
}
static func closedLinePathWithPoints(points: [Double], step: CGPoint) -> Path {
var path = Path()
if points.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)
}
path.addLine(to: CGPoint(x: point1.x, y: 0))
path.closeSubpath()
return path
}
}
extension CGPoint {
func point(to: CGPoint, x: CGFloat) -> CGPoint {
let a = (to.y - self.y) / (to.x - self.x)
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
for i in 0..<Int(steps) {
let t0 = CGFloat(i) / steps
let t1 = CGFloat(i+1) / steps
let a = point(to: to, t: t0, control: control)
let b = point(to: to, t: t1, control: control)
dist += a.line(to: b)
}
return dist
}
func quadCurve(to: CGPoint, control: CGPoint, x: CGFloat) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
for i in 0..<Int(steps) {
let t0 = CGFloat(i) / steps
let t1 = CGFloat(i+1) / steps
let a = point(to: to, t: t0, control: control)
let b = point(to: to, t: t1, control: control)
if a.x >= x {
return dist
} else if b.x > x {
dist += a.line(to: b, x: x)
return dist
} else if b.x == x {
dist += a.line(to: b)
return dist
}
dist += a.line(to: b)
}
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
for i in 0..<Int(steps) {
let t0 = CGFloat(i) / steps
let t1 = CGFloat(i+1) / steps
let a = point(to: to, t: t0, control1: control1, control2: control2)
let b = point(to: to, t: t1, control1: control1, control2: control2)
dist += a.line(to: b)
}
return dist
}
func curve(to: CGPoint, control1: CGPoint, control2: CGPoint, x: CGFloat) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
for i in 0..<Int(steps) {
let t0 = CGFloat(i) / steps
let t1 = CGFloat(i+1) / steps
let a = point(to: to, t: t0, control1: control1, control2: control2)
let b = point(to: to, t: t1, control1: control1, control2: control2)
if a.x >= x {
return dist
} else if b.x > x {
dist += a.line(to: b, x: x)
return dist
} else if b.x == x {
dist += a.line(to: b)
return dist
}
dist += a.line(to: b)
}
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
value += pow(1-t, 2) * x
value += 2 * (1-t) * t * c
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
value += pow(1-t, 3) * x
value += 3 * pow(1-t, 2) * t * control1
value += 3 * (1-t) * pow(t, 2) * control2
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)
if firstPoint.y < secondPoint.y {
controlPoint.y += diffY
} else if firstPoint.y > secondPoint.y {
controlPoint.y -= diffY
}
return controlPoint
}
}
@@ -0,0 +1,11 @@
import SwiftUI
extension View {
public func type<S>(_ type: S) -> some View where S: ChartType {
self.environment(\.chartType, AnyChartType(type))
}
public func style(_ style: ChartStyle) -> some View {
self.environment(\.chartStyle, style)
}
}
@@ -0,0 +1,43 @@
import SwiftUI
public enum ChartLabelSize: CGFloat {
case small = 16.0
case normal = 24.0
case large = 32.0
}
public enum ChartLabelType {
case title
case legend
}
public struct ChartLabel: View {
private let text: String
private let labelSize: ChartLabelSize
private let labelType: ChartLabelType
private var labelColor: Color {
switch labelType {
case .title:
return .black
case .legend:
return .gray
}
}
public init (_ text: String,
type: ChartLabelType = .title,
size: ChartLabelSize = .normal) {
self.text = text
labelType = type
labelSize = size
}
public var body: some View {
Text(self.text)
.font(.system(size: labelSize.rawValue))
.bold()
.foregroundColor(self.labelColor)
.padding([.top, .bottom], 16.0)
}
}
@@ -0,0 +1,28 @@
import SwiftUI
public struct ChartStyle {
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
}
}
@@ -0,0 +1,38 @@
import SwiftUI
public struct ColorGradient: Equatable {
public let startColor: Color
public let endColor: Color
public init(_ color: Color) {
self.startColor = color
self.endColor = color
}
public init (_ startColor: Color, _ endColor: Color) {
self.startColor = startColor
self.endColor = endColor
}
public var gradient: Gradient {
return Gradient(colors: [startColor, endColor])
}
}
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)
}
}
extension ColorGradient {
public static let orangeBright = ColorGradient(ChartColors.orangeBright)
public static let redBlack = ColorGradient(.red, .black)
public static let greenRed = ColorGradient(.green, .red)
public static let whiteBlack = ColorGradient(.white, .black)
}
@@ -0,0 +1,10 @@
import SwiftUI
public enum ChartColors {
// Orange
static let orangeBright = Color(hexString: "#FF782C")
static let orangeDark = Color(hexString: "#EC2301")
static let legendColor: Color = Color(hexString: "#E8E7EA")
static let indicatorKnob: Color = Color(hexString: "#FF57A6")
}
@@ -0,0 +1,30 @@
import SwiftUI
public struct BarChart: ChartType {
public func makeChart(configuration: Self.Configuration, style: Self.Style) -> some View {
BarChartRow(data: configuration.data, style: style)
}
public init() {}
}
struct BarChart_Previews: PreviewProvider {
static var previews: some View {
Group {
BarChart().makeChart(
configuration: .init(data: [0]),
style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack))
Group {
BarChart().makeChart(
configuration: .init(data: [1, 2, 3, 5, 1]),
style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack))
}.environment(\.colorScheme, .light)
Group {
BarChart().makeChart(
configuration: .init(data: [1, 2, 3]),
style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack))
}.environment(\.colorScheme, .dark)
}
}
}
@@ -0,0 +1,49 @@
import SwiftUI
public struct BarChartCell: View {
@State var value: Double
@State var index: Int = 0
@State var width: Float
@State var numberOfDataPoints: Int
var gradientColor: ColorGradient
var cellWidth: Double {
return Double(width)/(Double(numberOfDataPoints) * 1.5)
}
@State var scaleValue: Double = 0
@Binding var touchLocation: CGFloat
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.scaleValue), anchor: .bottom)
.onAppear {
self.scaleValue = self.value
}
.animation(Animation.spring().delay(self.touchLocation < 0 ? Double(self.index) * 0.04 : 0))
}
}
struct BarChartCell_Previews: PreviewProvider {
static var previews: some View {
Group {
BarChartCell(value: 0, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: .constant(CGFloat()))
Group {
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: .constant(CGFloat()))
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: .constant(CGFloat()))
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: .constant(CGFloat()))
}
Group {
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: .constant(CGFloat()))
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: .constant(CGFloat()))
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: .constant(CGFloat()))
}.environment(\.colorScheme, .dark)
}
}
}
@@ -0,0 +1,82 @@
import SwiftUI
public struct BarChartRow: View {
@State var data: [Double] = []
@State var touchLocation: CGFloat = -1.0
enum Constant {
static let spacing: CGFloat = 16.0
}
var style: ChartStyle
var maxValue: Double {
guard let max = data.max() else {
return 1
}
return max != 0 ? max : 1
}
public var body: some View {
GeometryReader { geometry in
HStack(alignment: .bottom,
spacing: (geometry.frame(in: .local).width - Constant.spacing) / CGFloat(self.data.count * 3)) {
ForEach(0..<self.data.count, id: \.self) { index in
BarChartCell(value: self.normalizedValue(index: index),
index: index,
width: Float(geometry.frame(in: .local).width - Constant.spacing),
numberOfDataPoints: self.data.count,
gradientColor: self.style.foregroundColor.rotate(for: index),
touchLocation: self.$touchLocation)
.scaleEffect(self.getScaleSize(touchLocation: self.touchLocation, index: index), anchor: .bottom)
.animation(.spring())
}
}
.padding([.top, .leading, .trailing], 10)
.gesture(DragGesture()
.onChanged({ value in
self.touchLocation = value.location.x/geometry.frame(in: .local).width
})
.onEnded({ value in
self.touchLocation = -1
})
)
}
}
func normalizedValue(index: Int) -> Double {
return Double(data[index])/Double(maxValue)
}
func getScaleSize(touchLocation: CGFloat, index: Int) -> CGSize {
if touchLocation > CGFloat(index)/CGFloat(self.data.count) &&
touchLocation < CGFloat(index+1)/CGFloat(self.data.count) {
return CGSize(width: 1.4, height: 1.1)
}
return CGSize(width: 1, height: 1)
}
}
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])
@@ -10,14 +10,14 @@ import SwiftUI
struct IndicatorPoint: View {
var body: some View {
ZStack{
ZStack {
Circle()
.fill(Colors.IndicatorKnob)
.fill(ChartColors.indicatorKnob)
Circle()
.stroke(Color.white, style: StrokeStyle(lineWidth: 4))
}
.frame(width: 14, height: 14)
.shadow(color: Colors.LegendColor, radius: 6, x: 0, y: 6)
.shadow(color: ChartColors.legendColor, radius: 6, x: 0, y: 6)
}
}
@@ -0,0 +1,119 @@
import SwiftUI
public struct Line: View {
@State var frame: CGRect = .zero
@State var data: [Double]
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: data)
}
var path: Path {
let points = data
if curvedLines {
return Path.quadCurvedPathWithPoints(points: points,
step: step,
globalOffset: nil)
}
return Path.linePathWithPoints(points: points, step: step)
}
var closedPath: Path {
let points = data
if curvedLines {
return Path.quadClosedCurvedPathWithPoints(points: points,
step: step,
globalOffset: nil)
}
return Path.closedLinePathWithPoints(points: points, step: step)
}
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))
}
}
.onAppear {
self.frame = geometry.frame(in: .local)
}
.gesture(DragGesture()
.onChanged({ value in
self.touchLocation = value.location
self.showIndicator = true
})
.onEnded({ value in
self.touchLocation = .zero
self.showIndicator = false
})
)
}
}
}
// MARK: - Private functions
extension Line {
private func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint {
let closest = self.path.point(to: touchLocation.x)
return closest
}
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 {
static var previews: some View {
Group {
Line(data: [1, 2, 3, 1, 2, 5, 7], style: blackLineStyle)
Line(data: [1, 2, 3, 1, 2, 5, 7], style: redLineStyle)
}
}
}
private let blackLineStyle = ChartStyle(backgroundColor: ColorGradient(.white), foregroundColor: ColorGradient(.black))
private let redLineStyle = ChartStyle(backgroundColor: .whiteBlack, foregroundColor: ColorGradient(.red))
@@ -0,0 +1,32 @@
import SwiftUI
public struct LineChart: ChartType {
public func makeChart(configuration: Self.Configuration, style: Self.Style) -> some View {
Line(data: configuration.data, style: style)
}
public init() {}
}
struct LineChart_Previews: PreviewProvider {
static var previews: some View {
Group {
LineChart().makeChart(
configuration: .init(data: [0]),
style: .init(backgroundColor: .white, foregroundColor: ColorGradient(.black)))
Group {
LineChart().makeChart(
configuration: .init(data: [1, 2, 3, 5, 1]),
style: .init(backgroundColor: .white, foregroundColor: ColorGradient(.black)))
}.environment(\.colorScheme, .light)
Group {
LineChart().makeChart(
configuration: .init(data: [1, 2, 3]),
style: .init(backgroundColor: .white, foregroundColor: ColorGradient.redBlack))
}.environment(\.colorScheme, .dark)
}
}
}
@@ -0,0 +1,63 @@
//
// PieChart.swift
// SwiftUICharts
//
// Created by Nicolas Savoini on 2020-05-24.
//
import SwiftUI
public struct PieChart: ChartType {
public func makeChart(configuration: Self.Configuration, style: Self.Style) -> some View {
PieChartRow(data: configuration.data, style: style)
}
public init() {}
}
struct PieChart_Previews: PreviewProvider {
static var previews: some View {
Group {
PieChart().makeChart(
configuration: .init(data: [0]),
style: styleOneColor)
Group {
PieChart().makeChart(
configuration: .init(data: [56, 78, 53, 65, 54]),
style: styleOneColor)
PieChart().makeChart(
configuration: .init(data: [56, 78, 53, 65, 54]),
style: styleTwoColor)
PieChart().makeChart(
configuration: .init(data: [1, 1, 1, 1, 1, 1]),
style: trivialPursuit)
}.environment(\.colorScheme, .light)
Group {
PieChart().makeChart(
configuration: .init(data: [56, 78, 53, 65, 54]),
style: styleOneColor)
PieChart().makeChart(
configuration: .init(data: [56, 78, 53, 65, 54]),
style: styleTwoColor)
PieChart().makeChart(
configuration: .init(data: [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)])
@@ -0,0 +1,116 @@
//
// PieChartCell.swift
// SwiftUICharts
//
// Created by Nicolas Savoini on 2020-05-24.
//
import SwiftUI
struct PieSlice: Identifiable {
var id = UUID()
var startDeg: Double
var endDeg: Double
var value: Double
}
public struct PieChartCell: View {
@State private var show: Bool = false
var rect: CGRect
var radius: CGFloat {
return min(rect.width, rect.height)/2
}
var startDeg: Double
var endDeg: Double
var path: Path {
var path = Path()
path.addArc(
center: rect.mid,
radius: self.radius,
startAngle: Angle(degrees: self.startDeg),
endAngle: Angle(degrees: self.endDeg),
clockwise: false)
path.addLine(to: rect.mid)
path.closeSubpath()
return path
}
var index: Int
// Section line border color
var backgroundColor: Color
// Section color
var accentColor: ColorGradient
public var body: some View {
Group {
path
.fill(self.accentColor.linearGradient(from: .bottom, to: .top))
.overlay(path.stroke(self.backgroundColor, lineWidth: (startDeg == 0 && endDeg == 0 ? 0 : 2)))
.scaleEffect(self.show ? 1 : 0)
.animation(Animation.spring().delay(Double(self.index) * 0.04))
.onAppear {
self.show = true
}
}
}
}
struct PieChartCell_Previews: PreviewProvider {
static var previews: some View {
Group {
GeometryReader { geometry in
PieChartCell(
rect: geometry.frame(in: .local),
startDeg: 00.0,
endDeg: 90.0,
index: 0,
backgroundColor: Color.red,
accentColor: ColorGradient.greenRed)
}.frame(width: 100, height: 100)
GeometryReader { geometry in
PieChartCell(
rect: geometry.frame(in: .local),
startDeg: 0.0,
endDeg: 90.0,
index: 0,
backgroundColor: Color.green,
accentColor: ColorGradient.redBlack)
}.frame(width: 100, height: 100)
GeometryReader { geometry in
PieChartCell(
rect: geometry.frame(in: .local),
startDeg: 100.0,
endDeg: 135.0,
index: 0,
backgroundColor: Color.black,
accentColor: ColorGradient.whiteBlack)
}.frame(width: 100, height: 100)
GeometryReader { geometry in
PieChartCell(
rect: geometry.frame(in: .local),
startDeg: 185.0,
endDeg: 290.0,
index: 1,
backgroundColor: Color.purple,
accentColor: ColorGradient(.purple))
}.frame(width: 100, height: 100)
GeometryReader { geometry in
PieChartCell(
rect: geometry.frame(in: .local),
startDeg: 0,
endDeg: 0,
index: 0,
backgroundColor: Color.purple,
accentColor: ColorGradient(.purple))
}.frame(width: 100, height: 100)
}.previewLayout(.fixed(width: 125, height: 125))
}
}
@@ -0,0 +1,81 @@
//
// PieChartRow.swift
// SwiftUICharts
//
// Created by Nicolas Savoini on 2020-05-24.
//
import SwiftUI
public struct PieChartRow: View {
var data: [Double]
var style: ChartStyle
var slices: [PieSlice] {
var tempSlices: [PieSlice] = []
var lastEndDeg: Double = 0
let maxValue: Double = data.reduce(0, +)
for slice in data {
let normalized: Double = Double(slice) / (maxValue == 0 ? 1 : maxValue)
let startDeg = lastEndDeg
let endDeg = lastEndDeg + (normalized * 360)
lastEndDeg = endDeg
tempSlices.append(PieSlice(startDeg: startDeg, endDeg: endDeg, value: slice))
}
return tempSlices
}
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)
)
}
}
}
}
}
struct PieChartRow_Previews: PreviewProvider {
static var previews: some View {
Group {
//Empty Array - Default Colors.OrangeStart
PieChartRow(
data: [8, 23, 32, 7, 23, 43],
style: defaultMultiColorChartStyle)
.frame(width: 100, height: 100)
PieChartRow(
data: [8, 23, 32, 7, 23, 43],
style: multiColorChartStyle)
.frame(width: 100, height: 100)
PieChartRow(
data: [0],
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,21 @@
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
}
}
}
@@ -0,0 +1,11 @@
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))
}
-141
View File
@@ -1,141 +0,0 @@
//
// File.swift
//
//
// Created by András Samu on 2019. 07. 19..
//
import Foundation
import SwiftUI
public struct Colors {
public static let color1:Color = Color(hexString: "#E2FAE7")
public static let color1Accent:Color = Color(hexString: "#72BF82")
public static let color2:Color = Color(hexString: "#EEF1FF")
public static let color2Accent:Color = Color(hexString: "#4266E8")
public static let color3:Color = Color(hexString: "#FCECEA")
public static let color3Accent:Color = Color(hexString: "#E1614C")
public static let OrangeStart:Color = Color(hexString: "#FF782C")
public static let OrangeEnd:Color = Color(hexString: "#EC2301")
public static let LegendText:Color = Color(hexString: "#A7A6A8")
public static let LegendColor:Color = Color(hexString: "#E8E7EA")
public static let IndicatorKnob:Color = Color(hexString: "#FF57A6")
public static let GradientUpperBlue:Color = Color(hexString: "#C2E8FF")
public static let GradinetUpperBlue1:Color = Color(hexString: "#A8E1FF")
public static let GradientPurple:Color = Color(hexString: "#7B75FF")
public static let GradientNeonBlue:Color = Color(hexString: "#6FEAFF")
public static let GradientLowerBlue:Color = Color(hexString: "#F1F9FF")
public static let DarkPurple:Color = Color(hexString: "#1B205E")
public static let BorderBlue:Color = Color(hexString: "#4EBCFF")
}
public struct Styles {
public static let lineChartStyleOne = ChartStyle(
backgroundColor: Color.white,
accentColor: Colors.OrangeStart,
secondGradientColor: Colors.OrangeEnd,
chartFormSize: Form.medium,
textColor: Color.black,
legendTextColor: Color.gray)
public static let barChartStyleOne = ChartStyle(
backgroundColor: Color.white,
accentColor: Colors.OrangeStart,
secondGradientColor: Colors.OrangeEnd,
chartFormSize: Form.medium,
textColor: Color.black,
legendTextColor: Color.gray)
public static let barChartStyleTwo = ChartStyle(
backgroundColor: Color.white,
accentColor: Colors.GradientNeonBlue,
secondGradientColor: Colors.GradientPurple,
chartFormSize: Form.medium,
textColor: Color.black,
legendTextColor: Color.gray)
public static let barChartStyleThree = ChartStyle(
backgroundColor: Color.black,
accentColor: Colors.GradientNeonBlue,
secondGradientColor: Colors.GradientPurple,
chartFormSize: Form.medium,
textColor: Color.white,
legendTextColor: Color.gray)
public static let pieChartStyleOne = ChartStyle(
backgroundColor: Color.white,
accentColor: Colors.OrangeStart,
secondGradientColor: Colors.OrangeEnd,
chartFormSize: Form.medium,
textColor: Color.black,
legendTextColor: Color.gray)
}
public struct Form {
public static let small = CGSize(width:180, height:120)
public static let medium = CGSize(width:180, height:240)
public static let large = CGSize(width:360, height:120)
public static let detail = CGSize(width:180, height:120)
}
public struct ChartStyle {
public var backgroundColor: Color
public var accentColor: Color
public var secondGradientColor: Color
public var chartFormSize: CGSize
public var textColor: Color
public var legendTextColor: Color
public init(backgroundColor: Color, accentColor: Color, secondGradientColor: Color, chartFormSize: CGSize, textColor: Color, legendTextColor: Color){
self.backgroundColor = backgroundColor
self.accentColor = accentColor
self.secondGradientColor = secondGradientColor
self.chartFormSize = chartFormSize
self.textColor = textColor
self.legendTextColor = legendTextColor
}
public init(formSize: CGSize){
self.backgroundColor = Color.white
self.accentColor = Colors.OrangeStart
self.secondGradientColor = Colors.OrangeEnd
self.chartFormSize = formSize
self.legendTextColor = Color.gray
self.textColor = Color.black
}
}
class ChartData: ObservableObject {
@Published var points: [Int] = [Int]()
@Published var currentPoint: Int? = nil
init(points:[Int]) {
self.points = points
}
}
class TestData{
static public var data:ChartData = ChartData(points: [37,72,51,22,39,47,66,85,50])
}
extension Color {
init(hexString: String) {
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int = UInt32()
Scanner(string: hex).scanHexInt32(&int)
let r, g, b: UInt32
switch hex.count {
case 3: // RGB (12-bit)
(r, g, b) = ((int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(r, g, b) = (int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(r, g, b) = (int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(r, g, b) = (0, 0, 0)
}
self.init(red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255)
}
}
@@ -1,70 +0,0 @@
//
// Legend.swift
// LineChart
//
// Created by András Samu on 2019. 09. 02..
// Copyright © 2019. András Samu. All rights reserved.
//
import SwiftUI
struct Legend: View {
@ObservedObject var data: ChartData
@Binding var frame: CGRect
@Binding var hideHorizontalLines: Bool
var stepWidth: CGFloat {
return frame.size.width / CGFloat(data.points.count-1)
}
var stepHeight: CGFloat {
return frame.size.height / CGFloat(data.points.max()! + data.points.min()!)
}
var body: some View {
ZStack(alignment: .topLeading){
ForEach((0...4), id: \.self) { height in
HStack(alignment: .center){
Text("\(self.getYLegend()![height])").offset(x: 0, y: (self.frame.height-CGFloat(self.getYLegend()![height])*self.stepHeight)-(self.frame.height/2))
.foregroundColor(Colors.LegendText)
.font(.caption)
self.line(atHeight: CGFloat(self.getYLegend()![height]), width: self.frame.width)
.stroke(Colors.LegendColor, style: StrokeStyle(lineWidth: 1.5, lineCap: .round, dash: [5,height == 0 ? 0 : 10]))
.opacity((self.hideHorizontalLines && height != 0) ? 0 : 1)
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
.animation(.easeOut(duration: 0.2))
.clipped()
}
}
}
}
func line(atHeight: CGFloat, width: CGFloat) -> Path {
var hLine = Path()
hLine.move(to: CGPoint(x:5, y: atHeight*stepHeight))
hLine.addLine(to: CGPoint(x: width, y: atHeight*stepHeight))
return hLine
}
func getYLegend() -> [Int]? {
guard let max = data.points.max() else { return nil }
guard let min = data.points.min() else { return nil }
if(min > 0){
let upperBound = ((max/10)+1) * 10
let step = upperBound/4
return [step * 0,step * 1, step * 2, step * 3, step * 4]
}
return nil
}
}
struct Legend_Previews: PreviewProvider {
static var previews: some View {
GeometryReader{ geometry in
Legend(data: TestData.data, frame: .constant(geometry.frame(in: .local)), hideHorizontalLines: .constant(false))
}.frame(width: 320, height: 200)
}
}
-159
View File
@@ -1,159 +0,0 @@
//
// Line.swift
// LineChart
//
// Created by András Samu on 2019. 08. 30..
// Copyright © 2019. András Samu. All rights reserved.
//
import SwiftUI
struct Line: View {
@ObservedObject var data: ChartData
@Binding var frame: CGRect
@Binding var touchLocation: CGPoint
@Binding var showIndicator: Bool
@State private var showFull: Bool = false
@State var showBackground: Bool = true
var stepWidth: CGFloat {
return frame.size.width / CGFloat(data.points.count-1)
}
var stepHeight: CGFloat {
return frame.size.height / CGFloat(data.points.max()! + data.points.min()!)
}
var path: Path {
return Path.quadCurvedPathWithPoints(points: data.points, step: CGPoint(x: stepWidth, y: stepHeight))
}
var closedPath: Path {
return Path.quadClosedCurvedPathWithPoints(points: data.points, step: CGPoint(x: stepWidth, y: stepHeight))
}
var body: some View {
ZStack {
if(self.showFull && self.showBackground){
self.closedPath
.fill(LinearGradient(gradient: Gradient(colors: [Colors.GradientUpperBlue, .white]), startPoint: .bottom, endPoint: .top))
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
.transition(.opacity)
.animation(.easeIn(duration: 1.6))
}
self.path
.trim(from: 0, to: self.showFull ? 1:0)
.stroke(LinearGradient(gradient: Gradient(colors: [Colors.GradientPurple, Colors.GradientNeonBlue]), startPoint: .leading, endPoint: .trailing) ,style: StrokeStyle(lineWidth: 3))
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
.animation(.easeOut(duration: 1.2))
.onAppear(){
self.showFull.toggle()
}.drawingGroup()
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))
}
}
}
func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint {
let percentage:CGFloat = min(max(touchLocation.x,0)/self.frame.width,1)
let closest = self.path.percentPoint(percentage)
return closest
}
}
extension CGPoint {
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(p1:CGPoint, p2:CGPoint) -> CGPoint {
return CGPoint(x:(p1.x + p2.x) / 2,y: (p1.y + p2.y) / 2)
}
static func controlPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint {
var controlPoint = CGPoint.midPointForPoints(p1:p1, p2:p2)
let diffY = abs(p2.y - controlPoint.y)
if (p1.y < p2.y){
controlPoint.y += diffY
} else if (p1.y > p2.y) {
controlPoint.y -= diffY
}
return controlPoint
}
}
extension Path {
static func quadCurvedPathWithPoints(points:[Int], step:CGPoint) -> Path {
var path = Path()
var p1 = CGPoint(x: 0, y: CGFloat(points[0])*step.y)
path.move(to: p1)
if(points.count < 2){
path.addLine(to: CGPoint(x: step.x, y: step.y*CGFloat(points[1])))
return path
}
for pointIndex in 1..<points.count {
let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]))
let midPoint = CGPoint.midPointForPoints(p1: p1, p2: p2)
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p1))
path.addQuadCurve(to: p2, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p2))
p1 = p2
}
return path
}
static func quadClosedCurvedPathWithPoints(points:[Int], step:CGPoint) -> Path {
var path = Path()
path.move(to: .zero)
var p1 = CGPoint(x: 0, y: CGFloat(points[0])*step.y)
path.addLine(to: p1)
if(points.count < 2){
path.addLine(to: CGPoint(x: step.x, y: step.y*CGFloat(points[1])))
return path
}
for pointIndex in 1..<points.count {
let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]))
let midPoint = CGPoint.midPointForPoints(p1: p1, p2: p2)
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p1))
path.addQuadCurve(to: p2, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p2))
p1 = p2
}
path.addLine(to: CGPoint(x: p1.x, y: 0))
path.closeSubpath()
return path
}
func percentPoint(_ percent: CGFloat) -> CGPoint {
// percent difference between points
let diff: CGFloat = 0.001
let comp: CGFloat = 1 - diff
// handle limits
let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
let f = pct > comp ? comp : pct
let t = pct > comp ? 1 : pct + diff
let tp = self.trimmedPath(from: f, to: t)
return CGPoint(x: tp.boundingRect.midX, y: tp.boundingRect.midY)
}
}
struct Line_Previews: PreviewProvider {
static var previews: some View {
GeometryReader{ geometry in
Line(data: TestData.data, frame: .constant(geometry.frame(in: .local)), touchLocation: .constant(CGPoint(x: 300, y: 12)), showIndicator: .constant(true))
}.frame(width: 320, height: 160)
}
}
@@ -1,107 +0,0 @@
//
// LineCard.swift
// LineChart
//
// Created by András Samu on 2019. 08. 31..
// Copyright © 2019. András Samu. All rights reserved.
//
import SwiftUI
struct LineChartView: View {
let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
@ObservedObject var data:ChartData
public var title: String
public var legend: String?
public var style: ChartStyle
@State private var touchLocation:CGPoint = .zero
@State private var showIndicatorDot: Bool = false
@State private var currentValue: Int = 2 {
didSet{
if (oldValue != self.currentValue && showIndicatorDot) {
selectionFeedbackGenerator.selectionChanged()
}
}
}
let frame = CGSize(width: 180, height: 120)
public init(data: [Int], title: String, legend: String? = nil, style: ChartStyle = Styles.lineChartStyleOne ){
self.data = ChartData(points: data)
self.title = title
self.legend = legend
self.style = style
}
var body: some View {
ZStack(alignment: .center){
RoundedRectangle(cornerRadius: 20).fill(self.style.backgroundColor).frame(width: frame.width, height: 240, alignment: .center).shadow(radius: 8)
VStack(alignment: .leading){
if(!self.showIndicatorDot){
VStack(alignment: .leading, spacing: 8){
Text(self.title).font(.title).bold().foregroundColor(self.style.textColor)
if (self.legend != nil){
Text(self.legend!).font(.callout).foregroundColor(self.style.legendTextColor)
}
HStack {
Image(systemName: "arrow.up")
Text("14%")
}
}
.transition(.opacity)
.animation(.easeIn(duration: 0.1))
.padding([.leading, .top])
}else{
HStack{
Spacer()
Text("\(self.currentValue)")
.font(.system(size: 41, weight: .bold, design: .default))
.offset(x: 0, y: 30)
Spacer()
}
.transition(.scale)
.animation(.spring())
}
Spacer()
GeometryReader{ geometry in
Line(data: self.data, frame: .constant(geometry.frame(in: .local)), touchLocation: self.$touchLocation, showIndicator: self.$showIndicatorDot)
}
.frame(width: frame.width, height: frame.height)
.clipShape(RoundedRectangle(cornerRadius: 20))
.offset(x: 0, y: 0)
}.frame(width: self.style.chartFormSize.width, height: self.style.chartFormSize.height)
}
.gesture(DragGesture()
.onChanged({ value in
self.touchLocation = value.location
self.showIndicatorDot = true
self.getClosestDataPoint(toPoint: value.location, width:self.frame.width, height: self.frame.height)
})
.onEnded({ value in
self.showIndicatorDot = false
})
)
}
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 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)
}
return .zero
}
}
struct WidgetView_Previews: PreviewProvider {
static var previews: some View {
Group {
LineChartView(data: [8,23,54,32,12,37,7,23,43], title: "Line chart", legend: "Basic")
.environment(\.colorScheme, .light)
}
}
}
@@ -1,65 +0,0 @@
//
// PieChartCell.swift
// ChartView
//
// Created by András Samu on 2019. 06. 12..
// Copyright © 2019. András Samu. All rights reserved.
//
import SwiftUI
struct PieSlice: Identifiable {
var id = UUID()
var startDeg: Double
var endDeg: Double
var value: Int
var normalizedValue: Double
}
public struct PieChartCell : View {
@State private var show:Bool = false
var rect: CGRect
var radius: CGFloat {
return min(rect.width, rect.height)/2
}
var startDeg: Double
var endDeg: Double
var path: Path {
var path = Path()
path.addArc(center:rect.mid , radius:self.radius, startAngle: Angle(degrees: self.startDeg), endAngle: Angle(degrees: self.endDeg), clockwise: false)
path.addLine(to: rect.mid)
path.closeSubpath()
return path
}
var index: Int
var backgroundColor:Color
var accentColor:Color
public var body: some View {
path
.fill()
.foregroundColor(self.accentColor)
.overlay(path.stroke(self.backgroundColor, lineWidth: 2))
.scaleEffect(self.show ? 1 : 0)
.animation(Animation.spring().delay(Double(self.index) * 0.04))
.onAppear(){
self.show = true
}
}
}
extension CGRect {
var mid: CGPoint {
return CGPoint(x:self.midX, y: self.midY)
}
}
#if DEBUG
struct PieChartCell_Previews : PreviewProvider {
static var previews: some View {
GeometryReader { geometry in
PieChartCell(rect: geometry.frame(in: .local),startDeg: 0.0,endDeg: 90.0, index: 0, backgroundColor: Color(red: 252.0/255.0, green: 236.0/255.0, blue: 234.0/255.0), accentColor: Color(red: 225.0/255.0, green: 97.0/255.0, blue: 76.0/255.0))
}.frame(width:100, height:100)
}
}
#endif
@@ -1,46 +0,0 @@
//
// PieChartRow.swift
// ChartView
//
// Created by András Samu on 2019. 06. 12..
// Copyright © 2019. András Samu. All rights reserved.
//
import SwiftUI
public struct PieChartRow : View {
var data: [Int]
var backgroundColor: Color
var accentColor: Color
var slices: [PieSlice] {
var tempSlices:[PieSlice] = []
var lastEndDeg:Double = 0
let maxValue = data.reduce(0, +)
for slice in data {
let normalized:Double = Double(slice)/Double(maxValue)
let startDeg = lastEndDeg
let endDeg = lastEndDeg + (normalized * 360)
lastEndDeg = endDeg
tempSlices.append(PieSlice(startDeg: startDeg, endDeg: endDeg, value: slice, normalizedValue: normalized))
}
return tempSlices
}
public var body: some View {
GeometryReader { geometry in
ZStack{
ForEach(0..<self.slices.count){ i in
PieChartCell(rect: geometry.frame(in: .local), startDeg: self.slices[i].startDeg, endDeg: self.slices[i].endDeg, index: i, backgroundColor: self.backgroundColor,accentColor: self.accentColor)
}
}
}
}
}
#if DEBUG
struct PieChartRow_Previews : PreviewProvider {
static var previews: some View {
PieChartRow(data:[8,23,54,32,12,37,7,23,43], backgroundColor: Color(red: 252.0/255.0, green: 236.0/255.0, blue: 234.0/255.0), accentColor: Color(red: 225.0/255.0, green: 97.0/255.0, blue: 76.0/255.0)).frame(width: 100, height: 100)
}
}
#endif
@@ -1,59 +0,0 @@
//
// PieChartView.swift
// ChartView
//
// Created by András Samu on 2019. 06. 12..
// Copyright © 2019. András Samu. All rights reserved.
//
import SwiftUI
public struct PieChartView : View {
public var data: [Int]
public var title: String
public var legend: String?
public var style: ChartStyle
public init(data: [Int], title: String, legend: String? = nil, style: ChartStyle = Styles.pieChartStyleOne ){
self.data = data
self.title = title
self.legend = legend
self.style = style
}
public var body: some View {
ZStack{
Rectangle()
.fill(self.style.backgroundColor)
.cornerRadius(20)
.shadow(color: Color.gray, radius: 12)
VStack(alignment: .leading){
HStack{
Text(self.title)
.font(.headline)
.foregroundColor(self.style.textColor)
Spacer()
Image(systemName: "chart.pie.fill")
.imageScale(.large)
.foregroundColor(self.style.legendTextColor)
}.padding()
PieChartRow(data: data, backgroundColor: self.style.backgroundColor, accentColor: self.style.accentColor)
.foregroundColor(self.style.accentColor).padding(self.legend != nil ? 0 : 12).offset(y:self.legend != nil ? 0 : -10)
if(self.legend != nil) {
Text(self.legend!)
.font(.headline)
.foregroundColor(self.style.legendTextColor)
.padding()
}
}
}.frame(width: self.style.chartFormSize.width, height: self.style.chartFormSize.height)
}
}
#if DEBUG
struct PieChartView_Previews : PreviewProvider {
static var previews: some View {
PieChartView(data:[56,78,53,65,54], title: "Title", legend: "Legend")
}
}
#endif
@@ -0,0 +1,44 @@
//
// File.swift
//
//
// Created by Nicolas Savoini on 2020-05-25.
//
@testable import SwiftUICharts
import XCTest
class ArrayExtensionTests: XCTestCase {
func testArrayRotatingIndexEmpty() {
let colors = [ColorGradient]()
XCTAssertEqual(colors.rotate(for: 0), ColorGradient.orangeBright)
}
func testArrayRotatingIndexOneValue() {
let colors = [ColorGradient.greenRed]
XCTAssertEqual(colors.rotate(for: 0), ColorGradient.greenRed)
XCTAssertEqual(colors.rotate(for: 1), ColorGradient.greenRed)
XCTAssertEqual(colors.rotate(for: 2), ColorGradient.greenRed)
}
func testArrayRotatingIndexLessValues() {
let colors = [ColorGradient.greenRed, ColorGradient.whiteBlack]
XCTAssertEqual(colors.rotate(for: 0), ColorGradient.greenRed)
XCTAssertEqual(colors.rotate(for: 1), ColorGradient.whiteBlack)
XCTAssertEqual(colors.rotate(for: 2), ColorGradient.greenRed)
XCTAssertEqual(colors.rotate(for: 3), ColorGradient.whiteBlack)
XCTAssertEqual(colors.rotate(for: 4), ColorGradient.greenRed)
}
func testArrayRotatingIndexMoreValues() {
let colors = [ColorGradient.greenRed, ColorGradient.whiteBlack, ColorGradient.orangeBright]
XCTAssertEqual(colors.rotate(for: 0), ColorGradient.greenRed)
XCTAssertEqual(colors.rotate(for: 1), ColorGradient.whiteBlack)
}
}
@@ -0,0 +1,32 @@
//
// CGPointExtensionTests.swift
// SwiftUIChartsTests
//
// Created by Adrian Bolinger on 5/24/20.
//
@testable import SwiftUICharts
import XCTest
class CGPointExtensionTests: XCTestCase {
static let twentyElementArray: [Double] = Array(repeating: Double.random(in: 1...100), count: 20)
func testGetStepWithOneElementArray() {
let frame = CGRect(x: 0, y: 0, width: 300, height: 300)
let oneElementArray: [Double] = [0.0]
XCTAssertEqual(CGPoint.getStep(frame: frame, data: oneElementArray), .zero)
}
func testGetStepWithMultiElementArrayWithNegativeValues() {
let frame = CGRect(x: 0, y: 0, width: 300, height: 300)
let multiElementArray: [Double] = [-5.0, 0.0, 5.0]
XCTAssertEqual(CGPoint.getStep(frame: frame, data: multiElementArray), CGPoint(x: 150.0, y: 27.0))
}
func testGetStepWithMultiElementArrayWithPositiveValues() {
let frame = CGRect(x: 0, y: 0, width: 300, height: 300)
let multiElementArray: [Double] = [5.0, 10.0, 15.0]
XCTAssertEqual(CGPoint.getStep(frame: frame, data: multiElementArray), CGPoint(x: 150.0, y: 13.5))
}
}
@@ -0,0 +1,56 @@
//
// ColorExtensionTests.swift
// SwiftUIChartsTests
//
// Created by Adrian Bolinger on 5/24/20.
//
@testable import SwiftUICharts
import SwiftUI
import XCTest
class ColorExtensionTests: XCTestCase {
func testTwentyFourBitRGBColors() {
let actualWhite = Color(hexString: "FFFFFF")
let expectedWhite = Color(red: 1, green: 1, blue: 1)
XCTAssertEqual(actualWhite, expectedWhite)
let actualBlack = Color(hexString: "000000")
let expectedBlack = Color(red: 0, green: 0, blue: 0)
XCTAssertEqual(actualBlack, expectedBlack)
let actualRed = Color(hexString: "FF0000")
let expectedRed = Color(red: 255/255, green: 0, blue: 0)
XCTAssertEqual(actualRed, expectedRed)
let actualGreen = Color(hexString: "00FF00")
let expectedGreen = Color(red: 0, green: 1, blue: 0)
XCTAssertEqual(actualGreen, expectedGreen)
let actualBlue = Color(hexString: "0000FF")
let expectedBlue = Color(red: 0, green: 0, blue: 1)
XCTAssertEqual(actualBlue, expectedBlue)
}
func testTwelveBitRGBColors() {
let actualWhite = Color(hexString: "FFF")
let expectedWhite = Color(red: 1, green: 1, blue: 1)
XCTAssertEqual(actualWhite, expectedWhite)
let actualBlack = Color(hexString: "000")
let expectedBlack = Color(red: 0, green: 0, blue: 0)
XCTAssertEqual(actualBlack, expectedBlack)
let actualRed = Color(hexString: "F00")
let expectedRed = Color(red: 255/255, green: 0, blue: 0)
XCTAssertEqual(actualRed, expectedRed)
let actualGreen = Color(hexString: "0F0")
let expectedGreen = Color(red: 0, green: 1, blue: 0)
XCTAssertEqual(actualGreen, expectedGreen)
let actualBlue = Color(hexString: "00F")
let expectedBlue = Color(red: 0, green: 0, blue: 1)
XCTAssertEqual(actualBlue, expectedBlue)
}
}
@@ -9,6 +9,6 @@ final class SwiftUIChartsTests: XCTestCase {
}
static var allTests = [
("testExample", testExample),
("testExample", testExample)
]
}
@@ -3,7 +3,7 @@ import XCTest
#if !canImport(ObjectiveC)
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(SwiftUIChartsTests.allTests),
testCase(SwiftUIChartsTests.allTests)
]
}
#endif
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 445 KiB