Compare commits

..

83 Commits

Author SHA1 Message Date
Andras Samu 422c5c0303 Prepare charts to display x and y values souch as a value for a given point 2020-08-24 16:48:49 +02:00
Roddy Munro c843e6bede Add custom string format for ChartLabel when interactionInProgress = true (#151)
* Dark/Light mode fixes (#148)

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

Also solves line chart background to appear white in dark mode

* Add custom string format for ChartLabel when interactionInProgress = true

Co-authored-by: Sagar Patel <s.72427patel@gmail.com>
2020-08-24 16:44:05 +02:00
Andras Samu 9a588ebe5e Add PieChart interaction PR changes to v2 2020-08-24 16:43:08 +02:00
Dan Wood 8ee353c93a Activity-type Rings charts (#161)
Co-authored-by: Dan Wood <danwood@users.noreply.github.com>
2020-08-24 16:31:17 +02:00
Dan Wood 51db5a067a Issue 99 documentation (#159)
* Starting on filling in documentation.

* First pass on most/all files

* more descriptions filled in

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

* more basic stuff filled in

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

* more explanations

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

Also solves line chart background to appear white in dark mode
2020-07-31 13:13:56 +02:00
Andras Samu 7fb2a0013c Fix cornerMasking on card view when no shadow is set 2020-07-29 18:49:57 +02:00
Andras Samu 3265d3e16b Add public modifier to ChartColors and add showShadow property 2020-07-25 19:32:05 +02:00
Andras Samu c46902dab8 Refactor Chart base (#143) 2020-07-25 18:56:58 +02:00
Andras Samu 57ac969092 Added ChartLabel interaction 2020-06-28 20:25:21 +02:00
Andras Samu dff16e8d2d Creating a data structure which propagets changes in data to the charts (#114)
* Creating a data structure which propagets changes in data to the charts

* Fixed appearing animation
2020-06-21 18:53:48 +02:00
Andras Samu f0eea58bd8 Add CardView and CardLabel (#111) 2020-05-30 18:07:49 +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
Andras Samu 8197fd6ae1 Added new interactive Bar chart
Added new Line chart (also interactive)
Added new color schemes
2019-09-11 22:01:11 +02:00
Andras Samu d6682253bd readme corrected 2019-07-19 09:58:05 +02:00
Andras Samu 4c48427383 Added small, medium and large size formats 2019-07-19 09:53:09 +02:00
Andras Samu fcc2870b14 created license 2019-07-18 15:23:31 +02:00
58 changed files with 2314 additions and 323 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"
@@ -10,5 +10,18 @@
<integer>0</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>SwiftUICharts</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>SwiftUIChartsTests</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>SwiftUICharts.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
</dict>
</dict>
</plist>
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Andras Samu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+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"])
]
)
+163 -13
View File
@@ -2,17 +2,18 @@
Swift package for displaying charts effortlessly.
![SwiftUI Charts](./chartview.gif "SwiftUI Charts")
![SwiftUI Charts](./Resources/showcase1.gif "SwiftUI Charts")
It supports currently:
* barcharts
* piecharts
It supports:
* Line charts
* Bar charts
* Pie charts
### 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:
@@ -20,23 +21,172 @@ 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:
Barchart:
### Demo
Added an example project, with **iOS, watchOS** target: https://github.com/AppPear/ChartViewDemo
## 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
ChartView(data: [8,23,54,32,12,37,7,23,43], title: "Barchart")
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()
```
Piechart:
Adopts to dark mode automatically
![Line Charts](./Resources/showcase3.gif "Line Charts")
You can add your custom darkmode style by specifying:
```swift
PieChartView(data:[43,56,78,34], title: "Piechart")
let myCustomStyle = ChartStyle(...)
let myCutsomDarkModeStyle = ChartStyle(...)
myCustomStyle.darkModeStyle = myCutsomDarkModeStyle
```
You can optionally configure:
* legend
**Line chart is interactive, so you can drag across to reveal the data points**
You can add a line chart with the following code:
```swift
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](./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: 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: 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:
Customizable:
* background color
* accent color
* second gradient color
* text color
* legend text color
```swift
ChartView(data: [12,17,24,33,36,31,27,23,14], title: "Title", legend: "Legend", backgroundColor:Color(red: 226.0/255.0, green: 250.0/255.0, blue: 231.0/255.0) , accentColor:Color(red: 114.0/255.0, green: 191.0/255.0, blue: 130.0/255.0))
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)
```
You can access built-in styles:
```swift
BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", style: Styles.barChartMidnightGreen)
```
#### All styles available as a preset:
* barChartStyleOrangeLight
* barChartStyleOrangeDark
* barChartStyleNeonBlueLight
* barChartStyleNeonBlueDark
* barChartMidnightGreenLight
* barChartMidnightGreenDark
![Midnightgreen](./Resources/midnightgreen.gif "Midnightgreen")
![Custom Charts](./Resources/showcase5.png "Custom Charts")
### You can customize the size of the chart with a ChartForm object:
**ChartForm**
* `.small`
* `.medium`
* `.large`
* `.detail`
```swift
BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", form: ChartForm.small)
```
### WatchOS support for Bar charts:
![Pie Charts](./Resources/watchos1.png "Pie Charts")
## Pie charts
![Pie Charts](./Resources/showcase4.png "Pie Charts")
You can add a pie chart with the following code:
```swift
PieChartView(data: [8,23,54,32], title: "Title", legend: "Legendary") // legend is optional
```
**Turn drop shadow off by adding to the Initialiser: `dropShadow: false`**
Binary file not shown.

After

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

@@ -0,0 +1,37 @@
import SwiftUI
/// View containing data and some kind of chart content
public struct CardView<Content: View>: View, ChartBase {
public var chartData = ChartData()
let content: () -> Content
private var showShadow: Bool
@EnvironmentObject var style: ChartStyle
/// Initialize with view options and a nested `ViewBuilder`
/// - Parameters:
/// - showShadow: should card have a rounded-rectangle shadow around it
/// - content: <#content description#>
public init(showShadow: Bool = true, @ViewBuilder content: @escaping () -> Content) {
self.showShadow = showShadow
self.content = content
}
/// The content and behavior of the `CardView`.
///
///
public var body: some View {
ZStack{
if showShadow {
RoundedRectangle(cornerRadius: 20)
.fill(Color.white)
.shadow(color: Color.gray, radius: 8)
}
VStack {
self.content()
}
.clipShape(RoundedRectangle(cornerRadius: showShadow ? 20 : 0))
}
}
}
@@ -0,0 +1,6 @@
import SwiftUI
/// Protocol for any type of chart, to get access to underlying data
public protocol ChartBase {
var chartData: ChartData { get }
}
@@ -0,0 +1,28 @@
import SwiftUI
/// An observable wrapper for an array of data for use in any chart
public class ChartData: ObservableObject {
@Published public var data: [(String, Double)] = []
var points: [Double] {
data.map { $0.1 }
}
var values: [String] {
data.map { $0.0 }
}
/// Initialize with data array
/// - Parameter data: Array of `Double`
public init(_ data: [Double]) {
self.data = data.map { ("", $0) }
}
public init(_ data: [(String, Double)]) {
self.data = data
}
public init() {
self.data = []
}
}
@@ -0,0 +1,7 @@
import SwiftUI
/// Representation of a single data point in a chart that is being observed
public class ChartValue: ObservableObject {
@Published var currentValue: Double = 0
@Published var interactionInProgress: Bool = false
}
@@ -0,0 +1,19 @@
import Foundation
extension Array where Element == ColorGradient {
/// <#Description#>
/// - Parameter index: offset in data table
/// - Returns: <#description#>
func rotate(for index: Int) -> ColorGradient {
if self.isEmpty {
return ColorGradient.orangeBright
}
if self.count <= index {
return self[index % self.count]
}
return self[index]
}
}
@@ -0,0 +1,41 @@
import SwiftUI
extension CGPoint {
/// Calculate X and Y delta for each data point, based on data min/max and enclosing frame.
/// - Parameters:
/// - frame: Rectangle of enclosing frame
/// - data: array of `Double`
/// - Returns: X and Y delta as a `CGPoint`
static func getStep(frame: CGRect, data: [Double]) -> CGPoint {
let padding: CGFloat = 30.0
// 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,11 @@
import Foundation
import SwiftUI
extension CGRect {
/// Midpoint of rectangle
/// - Returns: the coordinate for a rectangle center
public var mid: CGPoint {
return CGPoint(x: self.midX, y: self.midY)
}
}
@@ -0,0 +1,21 @@
import SwiftUI
extension View where Self: ChartBase {
/// Set data for a chart
/// - Parameter data: array of `Double`
/// - Returns: modified `View` with data attached
public func data(_ data: [Double]) -> some View {
chartData.data = data.map { ("", $0) }
return self
.environmentObject(chartData)
.environmentObject(ChartValue())
}
public func data(_ data: [(String, Double)]) -> some View {
chartData.data = data
return self
.environmentObject(chartData)
.environmentObject(ChartValue())
}
}
@@ -0,0 +1,25 @@
import SwiftUI
extension Color {
/// Create a `Color` from a hexadecimal representation
/// - Parameter hexString: 3, 6, or 8-character string, with optional (ignored) punctuation such as "#"
init(hexString: String) {
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int = UInt64()
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,476 @@
import SwiftUI
extension Path {
/// Returns a tiny segment of path based on percentage along the path
///
/// TODO: Explain why more than 1 gets 0 and why less than 0 gets 1
/// - Parameter percent: fraction along data set, between 0.0 and 1.0 (underflow and overflow are handled)
/// - Returns: tiny path right around the requested fraction
func trimmedPath(for percent: CGFloat) -> Path {
let boundsDistance: CGFloat = 0.001
let completion: CGFloat = 1 - boundsDistance
let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
// Start/end points centered around given percentage, but capped if right at the very end
let start = pct > completion ? completion : pct - boundsDistance
let end = pct > completion ? 1 : pct + boundsDistance
return trimmedPath(from: start, to: end)
}
/// Find the `CGPoint` for the given fraction along the path.
///
/// This works by requesting a very tiny trimmed section of the path, then getting the center of the bounds rectangle
/// - Parameter percent: fraction along data set, between 0.0 and 1.0 (underflow and overflow are handled)
/// - Returns: a `CGPoint` representing the location of that section of the path
func point(for percent: CGFloat) -> CGPoint {
let path = trimmedPath(for: percent)
return CGPoint(x: path.boundingRect.midX, y: path.boundingRect.midY)
}
/// <#Description#>
/// - Parameter maxX: <#maxX description#>
/// - Returns: <#description#>
func point(to maxX: CGFloat) -> CGPoint {
let total = length
let sub = length(to: maxX)
let percent = sub / total
return point(for: percent)
}
/// <#Description#>
/// - Returns: <#description#>
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
}
/// <#Description#>
/// - Parameter maxX: <#maxX description#>
/// - Returns: <#description#>
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
}
/// <#Description#>
/// - Parameters:
/// - points: <#points description#>
/// - step: <#step description#>
/// - globalOffset: <#globalOffset description#>
/// - Returns: <#description#>
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
}
/// <#Description#>
/// - Parameters:
/// - points: <#points description#>
/// - step: <#step description#>
/// - globalOffset: <#globalOffset description#>
/// - Returns: <#description#>
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
}
/// <#Description#>
/// - Parameters:
/// - points: <#points description#>
/// - step: <#step description#>
/// - Returns: <#description#>
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
}
/// <#Description#>
/// - Parameters:
/// - points: <#points description#>
/// - step: <#step description#>
/// - Returns: <#description#>
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 {
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - x: <#x description#>
/// - Returns: <#description#>
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)
}
/// <#Description#>
/// - Parameter to: <#to description#>
/// - Returns: <#description#>
func line(to: CGPoint) -> CGFloat {
dist(to: to)
}
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - x: <#x description#>
/// - Returns: <#description#>
func line(to: CGPoint, x: CGFloat) -> CGFloat {
dist(to: point(to: to, x: x))
}
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - control: <#control description#>
/// - Returns: <#description#>
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
}
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - control: <#control description#>
/// - x: <#x description#>
/// - Returns: <#description#>
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
}
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - t: <#t description#>
/// - control: <#control description#>
/// - Returns: <#description#>
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)
}
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - control1: <#control1 description#>
/// - control2: <#control2 description#>
/// - Returns: <#description#>
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
}
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - control1: <#control1 description#>
/// - control2: <#control2 description#>
/// - x: <#x description#>
/// - Returns: <#description#>
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
}
/// <#Description#>
/// - Parameters:
/// - to: <#to description#>
/// - t: <#t description#>
/// - control1: <#control1 description#>
/// - control2: <#control2 description#>
/// - Returns: <#description#>
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)
}
/// <#Description#>
/// - Parameters:
/// - x: <#x description#>
/// - y: <#y description#>
/// - t: <#t description#>
/// - c: <#c description#>
/// - Returns: <#description#>
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
}
/// <#Description#>
/// - Parameters:
/// - x: <#x description#>
/// - y: <#y description#>
/// - t: <#t description#>
/// - control1: <#control1 description#>
/// - control2: <#control2 description#>
/// - Returns: <#description#>
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
}
/// <#Description#>
/// - Parameters:
/// - point1: <#point1 description#>
/// - point2: <#point2 description#>
/// - Returns: <#description#>
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
)
}
/// <#Description#>
/// - Parameter to: <#to description#>
/// - Returns: <#description#>
func dist(to: CGPoint) -> CGFloat {
return sqrt((pow(self.x - to.x, 2) + pow(self.y - to.y, 2)))
}
/// <#Description#>
/// - Parameters:
/// - firstPoint: <#firstPoint description#>
/// - secondPoint: <#secondPoint description#>
/// - Returns: <#description#>
static func midPointForPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGPoint {
return CGPoint(
x: (firstPoint.x + secondPoint.x) / 2,
y: (firstPoint.y + secondPoint.y) / 2)
}
/// <#Description#>
/// - Parameters:
/// - firstPoint: <#firstPoint description#>
/// - secondPoint: <#secondPoint description#>
/// - Returns: <#description#>
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 {
/// Attach chart style to a View
/// - Parameter style: chart style
/// - Returns: `View` with chart style attached
public func chartStyle(_ style: ChartStyle) -> some View {
self.environmentObject(style)
}
}
@@ -0,0 +1,26 @@
import SwiftUI
/// <#Description#>
public struct ChartGrid<Content: View>: View, ChartBase {
public var chartData = ChartData()
let content: () -> Content
@EnvironmentObject var data: ChartData
@EnvironmentObject var style: ChartStyle
/// <#Description#>
/// - Parameter content: <#content description#>
public init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
/// The content and behavior of the `ChartGrid`.
///
/// TODO: Explain why this is in a `ZStack`
public var body: some View {
ZStack{
self.content()
}
}
}
@@ -0,0 +1,107 @@
import SwiftUI
/// What kind of label - this affects color, size, position of the label
public enum ChartLabelType {
case title
case subTitle
case largeTitle
case custom(size: CGFloat, padding: EdgeInsets, color: Color)
case legend
}
/// A chart may contain any number of labels in pre-set positions based on their `ChartLabelType`
public struct ChartLabel: View {
@EnvironmentObject var chartValue: ChartValue
@State var textToDisplay:String = ""
var format: String = "%.01f"
private var title: String
/// Label font size
/// - Returns: the font size of the label
private var labelSize: CGFloat {
switch labelType {
case .title:
return 32.0
case .legend:
return 14.0
case .subTitle:
return 24.0
case .largeTitle:
return 38.0
case .custom(let size, _, _):
return size
}
}
/// Padding around label
/// - Returns: the edge padding to use based on position of the label
private var labelPadding: EdgeInsets {
switch labelType {
case .title:
return EdgeInsets(top: 16.0, leading: 8.0, bottom: 0.0, trailing: 8.0)
case .legend:
return EdgeInsets(top: 4.0, leading: 8.0, bottom: 0.0, trailing: 8.0)
case .subTitle:
return EdgeInsets(top: 8.0, leading: 8.0, bottom: 0.0, trailing: 8.0)
case .largeTitle:
return EdgeInsets(top: 24.0, leading: 8.0, bottom: 0.0, trailing: 8.0)
case .custom(_, let padding, _):
return padding
}
}
/// Which type (color, size, position) for label
private let labelType: ChartLabelType
/// Foreground color for this label
/// - Returns: Color of label based on its `ChartLabelType`
private var labelColor: Color {
switch labelType {
case .title:
return Color(UIColor.label)
case .legend:
return Color(UIColor.secondaryLabel)
case .subTitle:
return Color(UIColor.label)
case .largeTitle:
return Color(UIColor.label)
case .custom(_, _, let color):
return color
}
}
/// Initialize
/// - Parameters:
/// - title: Any `String`
/// - type: Which `ChartLabelType` to use
public init (_ title: String,
type: ChartLabelType = .title,
format: String = "%.01f") {
self.title = title
labelType = type
self.format = format
}
/// The content and behavior of the `ChartLabel`.
///
/// Displays current value if chart is currently being touched along a data point, otherwise the specified text.
public var body: some View {
HStack {
Text(textToDisplay)
.font(.system(size: labelSize))
.bold()
.foregroundColor(self.labelColor)
.padding(self.labelPadding)
.onAppear {
self.textToDisplay = self.title
}
.onReceive(self.chartValue.objectWillChange) { _ in
self.textToDisplay = self.chartValue.interactionInProgress ? String(format: format, self.chartValue.currentValue) : self.title
}
if !self.chartValue.interactionInProgress {
Spacer()
}
}
}
}
@@ -0,0 +1,47 @@
import SwiftUI
/// Descripton of colors/styles for any kind of chart
public class ChartStyle: ObservableObject {
/// colors for background are of chart
public let backgroundColor: ColorGradient
/// colors for foreground fill of chart
public let foregroundColor: [ColorGradient]
/// Initialize with a single background color and an array of `ColorGradient` for the foreground
/// - Parameters:
/// - backgroundColor: a `Color`
/// - foregroundColor: array of `ColorGradient`
public init(backgroundColor: Color, foregroundColor: [ColorGradient]) {
self.backgroundColor = ColorGradient.init(backgroundColor)
self.foregroundColor = foregroundColor
}
/// Initialize with a single background color and a single `ColorGradient` for the foreground
/// - Parameters:
/// - backgroundColor: a `Color`
/// - foregroundColor: a `ColorGradient`
public init(backgroundColor: Color, foregroundColor: ColorGradient) {
self.backgroundColor = ColorGradient.init(backgroundColor)
self.foregroundColor = [foregroundColor]
}
/// Initialize with a single background `ColorGradient` and a single `ColorGradient` for the foreground
/// - Parameters:
/// - backgroundColor: a `ColorGradient`
/// - foregroundColor: a `ColorGradient`
public init(backgroundColor: ColorGradient, foregroundColor: ColorGradient) {
self.backgroundColor = backgroundColor
self.foregroundColor = [foregroundColor]
}
/// Initialize with a single background `ColorGradient` and an array of `ColorGradient` for the foreground
/// - Parameters:
/// - backgroundColor: a `ColorGradient`
/// - foregroundColor: array of `ColorGradient`
public init(backgroundColor: ColorGradient, foregroundColor: [ColorGradient]) {
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
}
}
@@ -0,0 +1,47 @@
import SwiftUI
/// An encapsulation of a simple gradient between one color and another
public struct ColorGradient: Equatable {
public let startColor: Color
public let endColor: Color
/// Initialize as a solid color
/// - Parameter color: a single `Color` (no gradient effect visible)
public init(_ color: Color) {
self.startColor = color
self.endColor = color
}
/// Initialize a color gradient from two specified colors
/// - Parameters:
/// - startColor: starting color
/// - endColor: ending color
public init(_ startColor: Color, _ endColor: Color) {
self.startColor = startColor
self.endColor = endColor
}
/// Convert to a `Gradient` object (more complicated than just two colors)
/// - Returns: a `Gradient` between the specified start and end colors
public var gradient: Gradient {
return Gradient(colors: [startColor, endColor])
}
}
extension ColorGradient {
/// Convenience method to return a SwiftUI LinearGradient view 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,11 @@
import SwiftUI
/// Some predefined colors, used for demos, defaults if color is missing, and data indicator point
public enum ChartColors {
// Orange
public static let orangeBright = Color(hexString: "#FF782C")
public static let orangeDark = Color(hexString: "#EC2301")
public static let legendColor: Color = Color(hexString: "#E8E7EA")
public static let indicatorKnob: Color = Color(hexString: "#FF57A6")
}
-41
View File
@@ -1,41 +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 ChartCell : View {
var value: Double
var index: Int = 0
var width: Float
var numberOfDataPoints: Int
var cellWidth: Double {
return Double(width)/(Double(numberOfDataPoints) * 1.5)
}
@State var scaleValue: Double = 0
public var body: some View {
ZStack {
Rectangle()
.cornerRadius(4)
}
.frame(width: CGFloat(self.cellWidth))
.scaleEffect(CGSize(width: 1, height: self.scaleValue), anchor: .bottom)
.onAppear(){
self.scaleValue = self.value
}
.animation(Animation.spring().delay(Double(self.index) * 0.04))
}
}
#if DEBUG
struct ChartCell_Previews : PreviewProvider {
static var previews: some View {
ChartCell(value: Double(0.75), width: 320, numberOfDataPoints: 12)
}
}
#endif
-33
View File
@@ -1,33 +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 ChartRow : View {
var data: [Int]
var maxValue: Int {
data.max() ?? 0
}
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
ChartCell(value: Double(self.data[i])/Double(self.maxValue), index: i, width: Float(geometry.frame(in: .local).width - 22), numberOfDataPoints: self.data.count)
}
}.padding([.trailing,.leading], 13)
}
}
}
#if DEBUG
struct ChartRow_Previews : PreviewProvider {
static var previews: some View {
ChartRow(data: [8,23,54,32,12,37,7])
}
}
#endif
-60
View File
@@ -1,60 +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 ChartView : View {
public var data: [Int]
public var title: String
public var legend: String?
public var backgroundColor:Color
public var accentColor:Color
public init(data: [Int], title: String, legend: String? = nil,backgroundColor:Color = Color(red: 238.0/255.0, green: 241.0/255.0, blue: 254.0/255.0),accentColor:Color = Color(red: 66.0/255.0, green: 102.0/255.0, blue: 232.0/255.0) ){
self.data = data
self.title = title
self.legend = legend
self.backgroundColor = backgroundColor
self.accentColor = accentColor
}
public var body: some View {
ZStack{
Rectangle()
.fill(self.backgroundColor)
.cornerRadius(20)
VStack(alignment: .leading){
HStack{
Text(self.title)
.font(.headline)
Spacer()
Image(systemName: "waveform.path.ecg")
.imageScale(.large)
.foregroundColor(self.accentColor)
}.padding()
ChartRow(data: data)
.foregroundColor(self.accentColor)
.clipped()
if self.legend != nil {
Text(self.legend!)
.font(.headline)
.foregroundColor(self.accentColor)
.padding()
}
}
}.frame(width: 180, height: 240)
}
}
#if DEBUG
struct ChartView_Previews : PreviewProvider {
static var previews: some View {
ChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title")
}
}
#endif
@@ -0,0 +1,18 @@
import SwiftUI
/// A type of chart that displays vertical bars for each data point
public struct BarChart: View, ChartBase {
public var chartData = ChartData()
@EnvironmentObject var data: ChartData
@EnvironmentObject var style: ChartStyle
/// The content and behavior of the `BarChart`.
///
///
public var body: some View {
BarChartRow(chartData: data, style: style)
}
public init() {}
}
@@ -0,0 +1,71 @@
import SwiftUI
/// A single vertical bar in a `BarChart`
public struct BarChartCell: View {
var value: Double
var index: Int = 0
var width: Float
var numberOfDataPoints: Int
var gradientColor: ColorGradient
var touchLocation: CGFloat
var cellWidth: Double {
return Double(width)/(Double(numberOfDataPoints) * 1.5)
}
@State private var firstDisplay: Bool = true
public init( value: Double,
index: Int = 0,
width: Float,
numberOfDataPoints: Int,
gradientColor: ColorGradient,
touchLocation: CGFloat) {
self.value = value
self.index = index
self.width = width
self.numberOfDataPoints = numberOfDataPoints
self.gradientColor = gradientColor
self.touchLocation = touchLocation
}
/// The content and behavior of the `BarChartCell`.
///
/// Animated when first displayed, using the `firstDisplay` variable, with an increasing delay through the data set.
public var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 4)
.fill(gradientColor.linearGradient(from: .bottom, to: .top))
}
.frame(width: CGFloat(self.cellWidth))
.scaleEffect(CGSize(width: 1, height: self.firstDisplay ? 0.0 : self.value), anchor: .bottom)
.onAppear {
self.firstDisplay = false
}
.onDisappear {
self.firstDisplay = true
}
.transition(.slide)
.animation(Animation.spring().delay(self.touchLocation < 0 || !firstDisplay ? Double(self.index) * 0.04 : 0))
}
}
struct BarChartCell_Previews: PreviewProvider {
static var previews: some View {
Group {
Group {
BarChartCell(value: 0, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat())
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat())
}
Group {
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat())
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat())
}.environment(\.colorScheme, .dark)
}
}
}
@@ -0,0 +1,89 @@
import SwiftUI
/// A single row of data, a view in a `BarChart`
public struct BarChartRow: View {
@EnvironmentObject var chartValue: ChartValue
@ObservedObject var chartData: ChartData
@State private var touchLocation: CGFloat = -1.0
enum Constant {
static let spacing: CGFloat = 16.0
}
var style: ChartStyle
var maxValue: Double {
guard let max = chartData.points.max() else {
return 1
}
return max != 0 ? max : 1
}
/// The content and behavior of the `BarChartRow`.
///
/// Shows each `BarChartCell` in an `HStack`; may be scaled up if it's the one currently being touched.
/// Not using a drawing group for optimizing animation.
/// As touched (dragged) the `touchLocation` is updated and the current value is highlighted.
public var body: some View {
GeometryReader { geometry in
HStack(alignment: .bottom,
spacing: (geometry.frame(in: .local).width - Constant.spacing) / CGFloat(self.chartData.data.count * 3)) {
ForEach(0..<self.chartData.data.count, id: \.self) { index in
BarChartCell(value: self.normalizedValue(index: index),
index: index,
width: Float(geometry.frame(in: .local).width - Constant.spacing),
numberOfDataPoints: self.chartData.data.count,
gradientColor: self.style.foregroundColor.rotate(for: index),
touchLocation: self.touchLocation)
.scaleEffect(self.getScaleSize(touchLocation: self.touchLocation, index: index), anchor: .bottom)
.animation(Animation.easeIn(duration: 0.2))
}
// .drawingGroup()
}
.padding([.top, .leading, .trailing], 10)
.gesture(DragGesture()
.onChanged({ value in
let width = geometry.frame(in: .local).width
self.touchLocation = value.location.x/width
if let currentValue = self.getCurrentValue(width: width) {
self.chartValue.currentValue = currentValue
self.chartValue.interactionInProgress = true
}
})
.onEnded({ value in
self.chartValue.interactionInProgress = false
self.touchLocation = -1
})
)
}
}
/// Value relative to maximum value
/// - Parameter index: index into array of data
/// - Returns: data value at given index, divided by data maximum
func normalizedValue(index: Int) -> Double {
return Double(chartData.points[index])/Double(maxValue)
}
/// Size to scale the touch indicator
/// - Parameters:
/// - touchLocation: fraction of width where touch is happening
/// - index: index into data array
/// - Returns: a scale larger than 1.0 if in bounds; 1.0 (unscaled) if not in bounds
func getScaleSize(touchLocation: CGFloat, index: Int) -> CGSize {
if touchLocation > CGFloat(index)/CGFloat(chartData.data.count) &&
touchLocation < CGFloat(index+1)/CGFloat(chartData.data.count) {
return CGSize(width: 1.4, height: 1.1)
}
return CGSize(width: 1, height: 1)
}
/// Get data value where touch happened
/// - Parameter width: width of chart
/// - Returns: value as `Double` if chart has data
func getCurrentValue(width: CGFloat) -> Double? {
guard self.chartData.data.count > 0 else { return nil}
let index = max(0,min(self.chartData.data.count-1,Int(floor((self.touchLocation*width)/(width/CGFloat(self.chartData.data.count))))))
return self.chartData.points[index]
}
}
@@ -0,0 +1,24 @@
import SwiftUI
/// A dot representing a single data point as user moves finger over line in `LineChart`
struct IndicatorPoint: View {
/// The content and behavior of the `IndicatorPoint`.
///
/// A filled circle with a thick white outline and a shadow
public var body: some View {
ZStack {
Circle()
.fill(ChartColors.indicatorKnob)
Circle()
.stroke(Color.white, style: StrokeStyle(lineWidth: 4))
}
.frame(width: 14, height: 14)
.shadow(color: ChartColors.legendColor, radius: 6, x: 0, y: 6)
}
}
struct IndicatorPoint_Previews: PreviewProvider {
static var previews: some View {
IndicatorPoint()
}
}
@@ -0,0 +1,183 @@
import SwiftUI
/// A single line of data, a view in a `LineChart`
public struct Line: View {
@EnvironmentObject var chartValue: ChartValue
@State private var frame: CGRect = .zero
@ObservedObject var chartData: ChartData
var style: ChartStyle
@State private var showIndicator: Bool = false
@State private var touchLocation: CGPoint = .zero
@State private var showFull: Bool = false
@State private var showBackground: Bool = true
var curvedLines: Bool = true
/// Step for plotting through data
/// - Returns: X and Y delta between each data point based on data and view's frame
var step: CGPoint {
return CGPoint.getStep(frame: frame, data: chartData.points)
}
/// Path of line graph
/// - Returns: A path for stroking representing the data, either curved or jagged.
var path: Path {
let points = chartData.points
if curvedLines {
return Path.quadCurvedPathWithPoints(points: points,
step: step,
globalOffset: nil)
}
return Path.linePathWithPoints(points: points, step: step)
}
/// Path of linegraph, but also closed at the bottom side
/// - Returns: A path for filling representing the data, either curved or jagged
var closedPath: Path {
let points = chartData.points
if curvedLines {
return Path.quadClosedCurvedPathWithPoints(points: points,
step: step,
globalOffset: nil)
}
return Path.closedLinePathWithPoints(points: points, step: step)
}
// see https://stackoverflow.com/a/62370919
// This lets geometry be recalculated when device rotates. However it doesn't cover issue of app changing
// from full screen to split view. Not possible in SwiftUI? Feedback submitted to apple FB8451194.
let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
.makeConnectable()
.autoconnect()
/// The content and behavior of the `Line`.
/// Draw the background if showing the full line (?) and the `showBackground` option is set. Above that draw the line, and then the data indicator if the graph is currently being touched.
/// On appear, set the frame so that the data graph metrics can be calculated. On a drag (touch) gesture, highlight the closest touched data point.
/// TODO: explain rotation
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)
}
.onReceive(orientationChanged) { _ in
// When we receive notification here, the geometry is still the old value
// so delay evaluation to get the new frame!
DispatchQueue.main.async {
self.frame = geometry.frame(in: .local) // recalculate layout with new frame
}
}
.gesture(DragGesture()
.onChanged({ value in
self.touchLocation = value.location
self.showIndicator = true
self.getClosestDataPoint(point: self.getClosestPointOnPath(touchLocation: value.location))
self.chartValue.interactionInProgress = true
})
.onEnded({ value in
self.touchLocation = .zero
self.showIndicator = false
self.chartValue.interactionInProgress = false
})
)
}
}
}
// MARK: - Private functions
extension Line {
/// Calculate point closest to where the user touched
/// - Parameter touchLocation: location in view where touched
/// - Returns: `CGPoint` of data point on chart
private func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint {
let closest = self.path.point(to: touchLocation.x)
return closest
}
/// Figure out where closest touch point was
/// - Parameter point: location of data point on graph, near touch location
private func getClosestDataPoint(point: CGPoint) {
let index = Int(round((point.x)/step.x))
if (index >= 0 && index < self.chartData.data.count){
self.chartValue.currentValue = self.chartData.points[index]
}
}
/// Get the view representing the filled in background below the chart, filled with the foreground color's gradient
///
/// TODO: explain rotations
/// - Returns: SwiftUI `View`
private func getBackgroundPathView() -> some View {
self.closedPath
.fill(LinearGradient(gradient: Gradient(colors: [
style.foregroundColor.first?.startColor ?? .white,
style.foregroundColor.first?.endColor ?? .white,
.clear]),
startPoint: .bottom,
endPoint: .top))
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
.opacity(0.2)
.transition(.opacity)
.animation(.easeIn(duration: 1.6))
}
/// Get the view representing the line stroked in the `foregroundColor`
///
/// TODO: Explain how `showFull` works
/// TODO: explain rotations
/// - Returns: SwiftUI `View`
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(chartData: ChartData([8, 23, 32, 7, 23, 43]), style: blackLineStyle)
Line(chartData: ChartData([8, 23, 32, 7, 23, 43]), style: redLineStyle)
}
}
}
/// Predefined style, black over white, for preview
private let blackLineStyle = ChartStyle(backgroundColor: ColorGradient(.white), foregroundColor: ColorGradient(.black))
/// Predefined stylem red over white, for preview
private let redLineStyle = ChartStyle(backgroundColor: .whiteBlack, foregroundColor: ColorGradient(.red))
@@ -0,0 +1,18 @@
import SwiftUI
/// A type of chart that displays a line connecting the data points
public struct LineChart: View, ChartBase {
public var chartData = ChartData()
@EnvironmentObject var data: ChartData
@EnvironmentObject var style: ChartStyle
/// The content and behavior of the `LineChart`.
///
///
public var body: some View {
Line(chartData: data, style: style)
}
public init() {}
}
@@ -0,0 +1,18 @@
import SwiftUI
/// A type of chart that displays a slice of "pie" for each data point
public struct PieChart: View, ChartBase {
public var chartData = ChartData()
@EnvironmentObject var data: ChartData
@EnvironmentObject var style: ChartStyle
/// The content and behavior of the `PieChart`.
///
///
public var body: some View {
PieChartRow(chartData: data, style: style)
}
public init() {}
}
@@ -0,0 +1,116 @@
import SwiftUI
/// One slice of a `PieChartRow`
struct PieSlice: Identifiable {
var id = UUID()
var startDeg: Double
var endDeg: Double
var value: Double
}
/// A single row of data, a view in a `PieChart`
public struct PieChartCell: View {
@State private var show: Bool = false
var rect: CGRect
var radius: CGFloat {
return min(rect.width, rect.height)/2
}
var startDeg: Double
var endDeg: Double
/// Path representing this slice
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
/// The content and behavior of the `PieChartCell`.
///
/// Fills and strokes with 2-pixel line (unless start/end degrees not yet set). Animates by scaling up to 100% when first appears.
public var body: some View {
Group {
path
.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,34 @@
import SwiftUI
func isPointInCircle(point: CGPoint, circleRect: CGRect) -> Bool {
let r = min(circleRect.width, circleRect.height) / 2
let center = CGPoint(x: circleRect.midX, y: circleRect.midY)
let dx = point.x - center.x
let dy = point.y - center.y
let distance = sqrt(dx * dx + dy * dy)
return distance <= r
}
func degree(for point: CGPoint, inCircleRect circleRect: CGRect) -> Double {
let center = CGPoint(x: circleRect.midX, y: circleRect.midY)
let dx = point.x - center.x
let dy = point.y - center.y
let acuteDegree = Double(atan(dy / dx)) * (180 / .pi)
let isInBottomRight = dx >= 0 && dy >= 0
let isInBottomLeft = dx <= 0 && dy >= 0
let isInTopLeft = dx <= 0 && dy <= 0
let isInTopRight = dx >= 0 && dy <= 0
if isInBottomRight {
return acuteDegree
} else if isInBottomLeft {
return 180 - abs(acuteDegree)
} else if isInTopLeft {
return 180 + abs(acuteDegree)
} else if isInTopRight {
return 360 - abs(acuteDegree)
}
return 0
}
@@ -0,0 +1,69 @@
import SwiftUI
/// A single "row" (slice) of data, a view in a `PieChart`
public struct PieChartRow: View {
@ObservedObject var chartData: ChartData
@EnvironmentObject var chartValue: ChartValue
var style: ChartStyle
var slices: [PieSlice] {
var tempSlices: [PieSlice] = []
var lastEndDeg: Double = 0
let maxValue: Double = chartData.points.reduce(0, +)
for slice in chartData.points {
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
}
@State private var currentTouchedIndex = -1 {
didSet {
if oldValue != currentTouchedIndex {
chartValue.interactionInProgress = currentTouchedIndex != -1
guard currentTouchedIndex != -1 else { return }
chartValue.currentValue = slices[currentTouchedIndex].value
}
}
}
public var body: some View {
GeometryReader { geometry in
ZStack {
ForEach(0..<self.slices.count) { index in
PieChartCell(
rect: geometry.frame(in: .local),
startDeg: self.slices[index].startDeg,
endDeg: self.slices[index].endDeg,
index: index,
backgroundColor: self.style.backgroundColor.startColor,
accentColor: self.style.foregroundColor.rotate(for: index)
)
.scaleEffect(currentTouchedIndex == index ? 1.1 : 1)
.animation(Animation.spring())
}
}
.gesture(DragGesture()
.onChanged({ value in
let rect = geometry.frame(in: .local)
let isTouchInPie = isPointInCircle(point: value.location, circleRect: rect)
if isTouchInPie {
let touchDegree = degree(for: value.location, inCircleRect: rect)
currentTouchedIndex = slices.firstIndex(where: { $0.startDeg < touchDegree && $0.endDeg > touchDegree }) ?? -1
} else {
currentTouchedIndex = -1
}
})
.onEnded({ value in
currentTouchedIndex = -1
})
)
}
}
}
@@ -0,0 +1,187 @@
//
// Ring.swift
// ChartViewV2Demo
//
// Created by Dan Wood on 8/20/20.
// Based on article and playground code by Frank Jia
// https://medium.com/@frankjia/creating-activity-rings-in-swiftui-11ef7d336676
import SwiftUI
extension Double {
func toRadians() -> Double {
return self * Double.pi / 180
}
func toCGFloat() -> CGFloat {
return CGFloat(self)
}
}
struct RingShape: Shape {
/// Helper function to convert percent values to angles in degrees
/// - Parameters:
/// - percent: percent, greater than 100 is OK
/// - startAngle: angle to add after converting
/// - Returns: angle in degrees
static func percentToAngle(percent: Double, startAngle: Double) -> Double {
(percent / 100 * 360) + startAngle
}
private var percent: Double
private var startAngle: Double
private let drawnClockwise: Bool
// This allows animations to run smoothly for percent values
var animatableData: Double {
get {
return percent
}
set {
percent = newValue
}
}
init(percent: Double = 100, startAngle: Double = -90, drawnClockwise: Bool = false) {
self.percent = percent
self.startAngle = startAngle
self.drawnClockwise = drawnClockwise
}
/// This draws a simple arc from the start angle to the end angle
///
/// - Parameter rect: The frame of reference for describing this shape.
/// - Returns: A path that describes this shape.
func path(in rect: CGRect) -> Path {
let width = rect.width
let height = rect.height
let radius = min(width, height) / 2
let center = CGPoint(x: width / 2, y: height / 2)
let endAngle = Angle(degrees: RingShape.percentToAngle(percent: self.percent, startAngle: self.startAngle))
return Path { path in
path.addArc(center: center, radius: radius, startAngle: Angle(degrees: startAngle), endAngle: endAngle, clockwise: drawnClockwise)
}
}
}
struct Ring: View {
private static let ShadowColor: Color = Color.black.opacity(0.2)
private static let ShadowRadius: CGFloat = 5
private static let ShadowOffsetMultiplier: CGFloat = ShadowRadius + 2
private let ringWidth: CGFloat
private let percent: Double
private let foregroundColor: ColorGradient
private let startAngle: Double = -90
private let touchLocation: CGFloat
private var gradientStartAngle: Double {
self.percent >= 100 ? relativePercentageAngle - 360 : startAngle
}
private var absolutePercentageAngle: Double {
RingShape.percentToAngle(percent: self.percent, startAngle: 0)
}
private var relativePercentageAngle: Double {
// Take into account the startAngle
absolutePercentageAngle + startAngle
}
private var lastGradientColor: Color {
self.foregroundColor.endColor
}
private var ringGradient: AngularGradient {
AngularGradient(
gradient: self.foregroundColor.gradient,
center: .center,
startAngle: Angle(degrees: self.gradientStartAngle),
endAngle: Angle(degrees: relativePercentageAngle)
)
}
init(ringWidth: CGFloat, percent: Double, foregroundColor: ColorGradient, touchLocation:CGFloat) {
self.ringWidth = ringWidth
self.percent = percent
self.foregroundColor = foregroundColor
self.touchLocation = touchLocation
}
var body: some View {
GeometryReader { geometry in
ZStack {
// Background for the ring. Use the final color with reduced opacity
RingShape()
.stroke(style: StrokeStyle(lineWidth: self.ringWidth))
.fill(lastGradientColor.opacity(0.142857))
// Foreground
RingShape(percent: self.percent, startAngle: self.startAngle)
.stroke(style: StrokeStyle(lineWidth: self.ringWidth, lineCap: .round))
.fill(self.ringGradient)
// End of ring with drop shadow
if self.getShowShadow(frame: geometry.size) {
Circle()
.fill(self.lastGradientColor)
.frame(width: self.ringWidth, height: self.ringWidth, alignment: .center)
.offset(x: self.getEndCircleLocation(frame: geometry.size).0,
y: self.getEndCircleLocation(frame: geometry.size).1)
.shadow(color: Ring.ShadowColor,
radius: Ring.ShadowRadius,
x: self.getEndCircleShadowOffset().0,
y: self.getEndCircleShadowOffset().1)
}
}
}
// Padding to ensure that the entire ring fits within the view size allocated
.padding(self.ringWidth / 2)
}
private func getEndCircleLocation(frame: CGSize) -> (CGFloat, CGFloat) {
// Get angle of the end circle with respect to the start angle
let angleOfEndInRadians: Double = relativePercentageAngle.toRadians()
let offsetRadius = min(frame.width, frame.height) / 2
return (offsetRadius * cos(angleOfEndInRadians).toCGFloat(), offsetRadius * sin(angleOfEndInRadians).toCGFloat())
}
private func getEndCircleShadowOffset() -> (CGFloat, CGFloat) {
let angleForOffset = absolutePercentageAngle + (self.startAngle + 90)
let angleForOffsetInRadians = angleForOffset.toRadians()
let relativeXOffset = cos(angleForOffsetInRadians)
let relativeYOffset = sin(angleForOffsetInRadians)
let xOffset = relativeXOffset.toCGFloat() * Ring.ShadowOffsetMultiplier
let yOffset = relativeYOffset.toCGFloat() * Ring.ShadowOffsetMultiplier
return (xOffset, yOffset)
}
private func getShowShadow(frame: CGSize) -> Bool {
if self.percent >= 100 {
return true
}
let circleRadius = min(frame.width, frame.height) / 2
let remainingAngleInRadians = (360 - absolutePercentageAngle).toRadians().toCGFloat()
return circleRadius * remainingAngleInRadians <= self.ringWidth
}
}
struct Ring_Previews: PreviewProvider {
static var previews: some View {
VStack {
Ring(
ringWidth: 50, percent: 5 ,
foregroundColor: ColorGradient(.green, .blue), touchLocation: -1.0
)
.frame(width: 200, height: 200)
Ring(
ringWidth: 20, percent: 110 ,
foregroundColor: ColorGradient(.red, .blue), touchLocation: -1.0
)
.frame(width: 200, height: 200)
}
}
}
@@ -0,0 +1,22 @@
//
// RingsChart.swift
// ChartViewV2Demo
//
// Created by Dan Wood on 8/20/20.
//
import SwiftUI
public struct RingsChart: View, ChartBase {
public var chartData = ChartData()
@EnvironmentObject var data: ChartData
@EnvironmentObject var style: ChartStyle
// TODO - should put background opacity, ring width & spacing as chart style values
public var body: some View {
RingsChartRow(width:10.0, spacing:5.0, chartData: data, style: style)
}
}
@@ -0,0 +1,133 @@
//
// RingsChartRow.swift
// ChartViewV2Demo
//
// Created by Dan Wood on 8/20/20.
//
import SwiftUI
public struct RingsChartRow: View {
var width : CGFloat
var spacing : CGFloat
@EnvironmentObject var chartValue: ChartValue
@ObservedObject var chartData: ChartData
@State var touchRadius: CGFloat = -1.0
var style: ChartStyle
public var body: some View {
GeometryReader { geometry in
ZStack {
// FIXME: Why is background circle offset strangely when frame isn't specified? See Preview below. Related to the .animation somehow ????
Circle()
.fill(RadialGradient(gradient: self.style.backgroundColor.gradient, center: .center, startRadius: min(geometry.size.width, geometry.size.height)/2.0, endRadius: 1.0))
ForEach(0..<self.chartData.data.count, id: \.self) { index in
let scaleUp = isRingScaled(size:geometry.size, touchRadius: self.touchRadius, index: index)
let scaledWidth = scaleUp ? self.width * 2.0 : self.width
let normalPadding = (width + spacing) * CGFloat(index)
let scaledDiff = (scaledWidth - width) / 2.0
let padding = min(normalPadding - scaledDiff,
min(geometry.size.width, geometry.size.height)/2.0 - width
// make sure it doesn't get to crazy value
)
Ring(ringWidth:scaledWidth, percent: self.chartData.points[index], foregroundColor:self.style.foregroundColor.rotate(for: index),
touchLocation: self.touchRadius)
.zIndex(scaleUp ? 1 : 0) // make sure zoomed one is on top
.padding(padding)
.animation(Animation.easeIn(duration: 0.5))
}
// .drawingGroup()
}
.gesture(DragGesture()
.onChanged({ value in
let frame = geometry.frame(in: .local)
let radius = min(frame.width, frame.height) / 2.0
let deltaX = value.location.x - frame.midX
let deltaY = value.location.y - frame.midY
self.touchRadius = sqrt(deltaX*deltaX + deltaY*deltaY) // Pythagorean equation
if let currentValue = self.getCurrentValue(maxRadius: radius) {
self.chartValue.currentValue = currentValue
self.chartValue.interactionInProgress = true
}
})
.onEnded({ value in
self.chartValue.interactionInProgress = false
self.touchRadius = -1
})
)
}
}
/// should we scale up the touched ring?
/// - Parameters:
/// - size: size of the view
/// - touchRadius: distance from center where touched
/// - index: which ring is being drawn
/// - Returns: size to scale up or just scale of 1 if not scaled up
func isRingScaled(size: CGSize, touchRadius: CGFloat, index: Int) -> Bool {
let radius = min(size.width, size.height) / 2.0
return index == self.touchedCircleIndex(maxRadius: radius)
}
/// Find which circle has been touched
/// - Parameter maxRadius: radius of overall view circle
/// - Returns: which circle index was touched, if found. 0 = outer, 1 = next one in, etc.
func touchedCircleIndex(maxRadius: CGFloat) -> Int? {
guard self.chartData.data.count > 0 else { return nil } // no data
// Pretend actual circle goes ½ the inter-ring spacing out, so that a touch
// is registered on either side of each ring
let radialDistanceFromEdge = (maxRadius + spacing/2) - self.touchRadius;
guard radialDistanceFromEdge >= 0 else { return nil } // touched outside of ring
let touchIndex = Int(floor(radialDistanceFromEdge / (width + spacing)))
if touchIndex >= self.chartData.data.count { return nil } // too far from outside, no ring
return touchIndex
}
/// Description
/// - Parameter maxRadius: radius of overall view circle
/// - Returns: percentage value of the touched circle, based on `touchRadius` if found
func getCurrentValue(maxRadius: CGFloat) -> Double? {
guard let index = self.touchedCircleIndex(maxRadius: maxRadius) else { return nil }
return self.chartData.points[index]
}
}
struct RingsChartRow_Previews: PreviewProvider {
static var previews: some View {
let multiStyle = ChartStyle(backgroundColor: ColorGradient(Color.black.opacity(0.05), Color.white),
foregroundColor:
[ColorGradient(.purple, .blue),
ColorGradient(.orange, .red),
ColorGradient(.green, .yellow),
])
return RingsChartRow(width:20.0, spacing:10.0, chartData: ChartData([25,50,75,100,125]), style: multiStyle)
// and why does this not get centered when frame isn't specified?
.frame(width:300, height:400)
}
}
-65
View File
@@ -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
-46
View File
@@ -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
-60
View File
@@ -1,60 +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 backgroundColor:Color
public var accentColor:Color
public init(data: [Int], title: String, legend: String? = nil, backgroundColor:Color = Color(red: 252.0/255.0, green: 236.0/255.0, blue: 234.0/255.0),accentColor:Color = Color(red: 225.0/255.0, green: 97.0/255.0, blue: 76.0/255.0)){
self.data = data
self.title = title
self.legend = legend
self.backgroundColor = backgroundColor
self.accentColor = accentColor
}
public var body: some View {
ZStack{
Rectangle()
.fill(self.backgroundColor)
.cornerRadius(20)
VStack(alignment: .leading){
HStack{
Text(self.title)
.font(.headline)
Spacer()
Image(systemName: "chart.pie.fill")
.imageScale(.large)
.foregroundColor(self.accentColor)
}.padding()
PieChartRow(data: data, backgroundColor: self.backgroundColor, accentColor: self.accentColor)
.foregroundColor(self.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.accentColor)
.padding()
}
}
}.frame(width: 200, height: 240)
}
}
#if DEBUG
struct PieChartView_Previews : PreviewProvider {
static var previews: some View {
PieChartView(data:[56,78,53], 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: 170 KiB