Compare commits

..

92 Commits

Author SHA1 Message Date
“Andras 39ffa831df feat: add animation toggle interface 2022-11-26 14:35:49 +01:00
Andras Samu 7fd5b185f8 Feat/new protocol and range (#255)
* feat: new protocol for chained functions, and added support for explicit Y ranges. X coming as well

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

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

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

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

* Dark/Light mode fixes (#148)

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

Also solves line chart background to appear white in dark mode

* Add custom string format for ChartLabel when interactionInProgress = true

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

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

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

* First pass on most/all files

* more descriptions filled in

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

* more basic stuff filled in

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

* more explanations

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

Also solves line chart background to appear white in dark mode
2020-07-31 13:13:56 +02:00
Andras Samu 7fb2a0013c Fix cornerMasking on card view when no shadow is set 2020-07-29 18:49:57 +02:00
Andras Samu 3265d3e16b Add public modifier to ChartColors and add showShadow property 2020-07-25 19:32:05 +02:00
Andras Samu c46902dab8 Refactor Chart base (#143) 2020-07-25 18:56:58 +02:00
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
Andras Samu 0aebcf4abc Added dynamic scaling to bar-chart 2019-06-26 21:06:08 +02:00
Andras Samu ef58fd9560 screenshot 2019-06-14 15:27:22 +02:00
Andras Samu 1ab98cba42 Updated readme with instructions 2019-06-14 15:19:55 +02:00
83 changed files with 2829 additions and 300 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>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "91E23D30-CB6C-44DA-BEFC-9D39A1DA2242"
type = "1"
version = "2.0">
</Bucket>
@@ -0,0 +1,27 @@
<?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>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>
+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"])
]
)
+190 -1
View File
@@ -1,3 +1,192 @@
# SwiftUICharts
A description of this package.
Swift package for displaying charts effortlessly.
![SwiftUI Charts](./Resources/showcase1.gif "SwiftUI Charts")
It supports:
* Line charts
* Bar charts
* Pie charts
### Installation:
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`
### Usage:
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
**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**
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
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,98 @@
import SwiftUI
public struct AxisLabels<Content: View>: View {
struct YAxisViewKey: ViewPreferenceKey { }
struct ChartViewKey: ViewPreferenceKey { }
var axisLabelsData = AxisLabelsData()
var axisLabelsStyle = AxisLabelsStyle()
@State private var yAxisWidth: CGFloat = 25
@State private var chartWidth: CGFloat = 0
@State private var chartHeight: CGFloat = 0
let content: () -> Content
public init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var yAxis: some View {
VStack(spacing: 0.0) {
ForEach(Array(axisLabelsData.axisYLabels.reversed().enumerated()), id: \.element) { index, axisYData in
Text(axisYData)
.font(axisLabelsStyle.axisFont)
.foregroundColor(axisLabelsStyle.axisFontColor)
.frame(height: getYHeight(index: index,
chartHeight: chartHeight,
count: axisLabelsData.axisYLabels.count),
alignment: getYAlignment(index: index, count: axisLabelsData.axisYLabels.count))
}
}
.padding([.leading, .trailing], 4.0)
.background(ViewGeometry<YAxisViewKey>())
.onPreferenceChange(YAxisViewKey.self) { value in
yAxisWidth = value.first?.size.width ?? 0.0
}
}
func xAxis(chartWidth: CGFloat) -> some View {
HStack(spacing: 0.0) {
ForEach(Array(axisLabelsData.axisXLabels.enumerated()), id: \.element) { index, axisXData in
Text(axisXData)
.font(axisLabelsStyle.axisFont)
.foregroundColor(axisLabelsStyle.axisFontColor)
.frame(width: chartWidth / CGFloat(axisLabelsData.axisXLabels.count - 1))
}
}
.frame(height: 24.0, alignment: .top)
}
var chart: some View {
self.content()
.background(ViewGeometry<ChartViewKey>())
.onPreferenceChange(ChartViewKey.self) { value in
chartWidth = value.first?.size.width ?? 0.0
chartHeight = value.first?.size.height ?? 0.0
}
}
public var body: some View {
VStack(spacing: 0.0) {
HStack {
if axisLabelsStyle.axisLabelsYPosition == .leading {
yAxis
} else {
Spacer(minLength: yAxisWidth)
}
chart
if axisLabelsStyle.axisLabelsYPosition == .leading {
Spacer(minLength: yAxisWidth)
} else {
yAxis
}
}
xAxis(chartWidth: chartWidth)
}
}
private func getYHeight(index: Int, chartHeight: CGFloat, count: Int) -> CGFloat {
if index == 0 || index == count - 1 {
return chartHeight / (CGFloat(count - 1) * 2) + 10
}
return chartHeight / CGFloat(count - 1)
}
private func getYAlignment(index: Int, count: Int) -> Alignment {
if index == 0 {
return .top
}
if index == count - 1 {
return .bottom
}
return .center
}
}
@@ -0,0 +1,57 @@
import SwiftUI
extension AxisLabels {
public func setAxisYLabels(_ labels: [String],
position: AxisLabelsYPosition = .leading) -> AxisLabels {
self.axisLabelsData.axisYLabels = labels
self.axisLabelsStyle.axisLabelsYPosition = position
return self
}
public func setAxisXLabels(_ labels: [String]) -> AxisLabels {
self.axisLabelsData.axisXLabels = labels
return self
}
public func setAxisYLabels(_ labels: [(Double, String)],
range: ClosedRange<Int>,
position: AxisLabelsYPosition = .leading) -> AxisLabels {
let overreach = range.overreach + 1
var labelArray = [String](repeating: "", count: overreach)
labels.forEach {
let index = Int($0.0) - range.lowerBound
if labelArray[safe: index] != nil {
labelArray[index] = $0.1
}
}
self.axisLabelsData.axisYLabels = labelArray
self.axisLabelsStyle.axisLabelsYPosition = position
return self
}
public func setAxisXLabels(_ labels: [(Double, String)], range: ClosedRange<Int>) -> AxisLabels {
let overreach = range.overreach + 1
var labelArray = [String](repeating: "", count: overreach)
labels.forEach {
let index = Int($0.0) - range.lowerBound
if labelArray[safe: index] != nil {
labelArray[index] = $0.1
}
}
self.axisLabelsData.axisXLabels = labelArray
return self
}
public func setColor(_ color: Color) -> AxisLabels {
self.axisLabelsStyle.axisFontColor = color
return self
}
public func setFont(_ font: Font) -> AxisLabels {
self.axisLabelsStyle.axisFont = font
return self
}
}
@@ -0,0 +1,11 @@
import Foundation
public enum AxisLabelsYPosition {
case leading
case trailing
}
public enum AxisLabelsXPosition {
case top
case bottom
}
@@ -0,0 +1,11 @@
import SwiftUI
public final class AxisLabelsStyle: ObservableObject {
@Published public var axisFont: Font = .callout
@Published public var axisFontColor: Color = .primary
@Published var axisLabelsYPosition: AxisLabelsYPosition = .leading
@Published var axisLabelsXPosition: AxisLabelsXPosition = .bottom
public init() {
// no-op
}
}
@@ -0,0 +1,10 @@
import SwiftUI
public final class AxisLabelsData: ObservableObject {
@Published public var axisYLabels: [String] = []
@Published public var axisXLabels: [String] = []
public init() {
// no-op
}
}
@@ -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(white: 0.9, opacity: 1), radius: 8)
}
VStack (alignment: .leading) {
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: View {
var chartData: ChartData { get }
}
@@ -0,0 +1,74 @@
import SwiftUI
/// An observable wrapper for an array of data for use in any chart
public class ChartData: ObservableObject {
@Published public var data: [(Double, Double)] = []
public var rangeY: ClosedRange<Double>?
public var rangeX: ClosedRange<Double>?
var points: [Double] {
data.filter { rangeX?.contains($0.0) ?? true }.map { $0.1 }
}
var values: [Double] {
data.filter { rangeX?.contains($0.0) ?? true }.map { $0.0 }
}
var normalisedPoints: [Double] {
let absolutePoints = points.map { abs($0) }
var maxPoint = absolutePoints.max()
if let rangeY = rangeY {
maxPoint = Double(rangeY.overreach)
return points.map { ($0 - rangeY.lowerBound) / (maxPoint ?? 1.0) }
}
return points.map { $0 / (maxPoint ?? 1.0) }
}
var normalisedValues: [Double] {
let absoluteValues = values.map { abs($0) }
var maxValue = absoluteValues.max()
if let rangeX = rangeX {
maxValue = Double(rangeX.overreach)
return values.map { ($0 - rangeX.lowerBound) / (maxValue ?? 1.0) }
}
return values.map { $0 / (maxValue ?? 1.0) }
}
var normalisedData: [(Double, Double)] {
Array(zip(normalisedValues, normalisedPoints))
}
var normalisedYRange: Double {
return rangeY == nil ? (normalisedPoints.max() ?? 0.0) - (normalisedPoints.min() ?? 0.0) : 1
}
var normalisedXRange: Double {
return rangeX == nil ? (normalisedValues.max() ?? 0.0) - (normalisedValues.min() ?? 0.0) : 1
}
var isInNegativeDomain: Bool {
if let rangeY = rangeY {
return rangeY.lowerBound < 0
}
return (points.min() ?? 0.0) < 0
}
/// Initialize with data array
/// - Parameter data: Array of `Double`
public init(_ data: [Double], rangeY: ClosedRange<FloatLiteralType>? = nil) {
self.data = data.enumerated().map{ (index, value) in (Double(index), value) }
self.rangeY = rangeY
}
public init(_ data: [(Double, Double)], rangeY: ClosedRange<FloatLiteralType>? = nil) {
self.data = data
self.rangeY = rangeY
}
public init() {
self.data = []
}
}
@@ -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,10 @@
import SwiftUI
public struct ViewGeometry<T>: View where T: PreferenceKey {
public var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: T.self, value: [ViewSizeData(size: geometry.size)] as! T.Value)
}
}
}
@@ -0,0 +1,15 @@
import SwiftUI
public protocol ViewPreferenceKey: PreferenceKey {
typealias Value = [ViewSizeData]
}
public extension ViewPreferenceKey {
static var defaultValue: [ViewSizeData] {
[]
}
static func reduce(value: inout [ViewSizeData], nextValue: () -> [ViewSizeData]) {
value.append(contentsOf: nextValue())
}
}
@@ -0,0 +1,14 @@
import SwiftUI
public struct ViewSizeData: Identifiable, Equatable, Hashable {
public let id: UUID = UUID()
public let size: CGSize
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
@@ -0,0 +1,26 @@
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]
}
}
extension Collection {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
@@ -0,0 +1,47 @@
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 = 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)
}
func denormalize(with geometry: GeometryProxy) -> CGPoint {
let width = geometry.frame(in: .local).width
let height = geometry.frame(in: .local).height
return CGPoint(x: self.x * width, y: self.y * height)
}
}
@@ -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,23 @@
import SwiftUI
extension ChartBase {
public func data(_ data: [Double]) -> some ChartBase {
chartData.data = data.enumerated().map{ (index, value) in (Double(index), value) }
return self
}
public func data(_ data: [(Double, Double)]) -> some ChartBase {
chartData.data = data
return self
}
public func rangeY(_ range: ClosedRange<FloatLiteralType>) -> some ChartBase{
chartData.rangeY = range
return self
}
public func rangeX(_ range: ClosedRange<FloatLiteralType>) -> some ChartBase{
chartData.rangeX = range
return self
}
}
@@ -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,422 @@
import SwiftUI
extension Path {
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)
}
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 quadCurvedPathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if data.count < 2 {
return path
}
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
var point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
path.move(to: point1)
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
let midPoint = CGPoint.midPointForPoints(firstPoint: point1, secondPoint: point2)
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point1))
path.addQuadCurve(to: point2, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point2))
point1 = point2
}
return path
}
static func drawChartMarkers(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
let filteredData = data.filter { $0.1 <= 1 && $0.1 >= 0 }
if filteredData.count < 1 {
return path
}
let convertedXValues = filteredData.map { CGFloat($0.0) * rect.width }
let convertedYPoints = filteredData.map { CGFloat($0.1) * rect.height }
let markerSize = CGSize(width: 8, height: 8)
for pointIndex in 0..<filteredData.count {
path.addRoundedRect(in: CGRect(origin: CGPoint(x: convertedXValues[pointIndex] - markerSize.width / 2,
y: convertedYPoints[pointIndex] - markerSize.height / 2),
size: markerSize),
cornerSize: CGSize(width: markerSize.width / 2,
height: markerSize.height / 2))
}
return path
}
static func drawGridLines(numberOfHorizontalLines: Int, numberOfVerticalLines: Int, in rect: CGRect) -> Path {
var path = Path()
for index in 0..<numberOfHorizontalLines {
let normalisedSpacing = 1.0 / CGFloat(numberOfHorizontalLines - 1)
let startPoint = CGPoint(x: 0, y: normalisedSpacing * CGFloat(index) * rect.height)
let endPoint = CGPoint(x: rect.width, y: normalisedSpacing * CGFloat(index) * rect.height)
path.move(to: startPoint)
path.addLine(to: endPoint)
}
for index in 0..<numberOfVerticalLines {
let normalisedSpacing = 1.0 / CGFloat(numberOfVerticalLines - 1)
let startPoint = CGPoint(x: normalisedSpacing * CGFloat(index) * rect.width, y: 0)
let endPoint = CGPoint(x: normalisedSpacing * CGFloat(index) * rect.width, y: rect.height)
path.move(to: startPoint)
path.addLine(to: endPoint)
}
return path
}
static func quadClosedCurvedPathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if data.count < 2 {
return path
}
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
path.move(to: CGPoint(x: convertedXValues[0], y: 0))
var point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
path.addLine(to: point1)
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
let midPoint = CGPoint.midPointForPoints(firstPoint: point1, secondPoint: point2)
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point1))
path.addQuadCurve(to: point2, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point2))
point1 = point2
}
path.addLine(to: CGPoint(x: point1.x, y: 0))
path.closeSubpath()
return path
}
static func linePathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if data.count < 2 {
return path
}
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
let point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
path.move(to: point1)
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
path.addLine(to: point2)
}
return path
}
static func closedLinePathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if data.count < 2 {
return path
}
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
path.move(to: .zero)
let point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
path.addLine(to: point1)
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
path.addLine(to: point2)
}
path.addLine(to: CGPoint(x: point1.x, y: 0))
path.closeSubpath()
return path
}
}
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,7 @@
import Foundation
public extension ClosedRange where Bound: AdditiveArithmetic {
var overreach: Bound {
self.upperBound - self.lowerBound
}
}
@@ -0,0 +1,17 @@
import SwiftUI
extension Shape {
func fill<Fill: ShapeStyle, Stroke: ShapeStyle>(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View {
self
.stroke(strokeStyle, lineWidth: lineWidth)
.background(self.fill(fillStyle))
}
}
extension InsettableShape {
func fill<Fill: ShapeStyle, Stroke: ShapeStyle>(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View {
self
.strokeBorder(strokeStyle, lineWidth: lineWidth)
.background(self.fill(fillStyle))
}
}
@@ -0,0 +1,17 @@
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)
}
public func toStandardCoordinateSystem() -> some View {
self
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
}
}
@@ -0,0 +1,24 @@
import SwiftUI
public struct ChartGrid<Content: View>: View {
let content: () -> Content
public var gridOptions = GridOptions()
public init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
public var body: some View {
ZStack {
ChartGridShape(numberOfHorizontalLines: gridOptions.numberOfHorizontalLines,
numberOfVerticalLines: gridOptions.numberOfVerticalLines)
.stroke(gridOptions.color, style: gridOptions.strokeStyle)
if gridOptions.showBaseLine {
ChartGridBaseShape()
.stroke(gridOptions.color, style: gridOptions.baseStrokeStyle)
.rotationEffect(.degrees(180), anchor: .center)
}
self.content()
}
}
}
@@ -0,0 +1,18 @@
import SwiftUI
struct ChartGridBaseShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: 0))
return path
}
}
struct ChartGridBaseShape_Previews: PreviewProvider {
static var previews: some View {
ChartGridBaseShape()
.stroke()
.rotationEffect(.degrees(180), anchor: .center)
}
}
@@ -0,0 +1,28 @@
import SwiftUI
struct ChartGridShape: Shape {
var numberOfHorizontalLines: Int
var numberOfVerticalLines: Int
func path(in rect: CGRect) -> Path {
let path = Path.drawGridLines(numberOfHorizontalLines: numberOfHorizontalLines,
numberOfVerticalLines: numberOfVerticalLines,
in: rect)
return path
}
}
struct ChartGridShape_Previews: PreviewProvider {
static var previews: some View {
Group {
ChartGridShape(numberOfHorizontalLines: 5, numberOfVerticalLines: 0)
.stroke()
.toStandardCoordinateSystem()
ChartGridShape(numberOfHorizontalLines: 4, numberOfVerticalLines: 4)
.stroke()
.toStandardCoordinateSystem()
}
.padding()
}
}
@@ -0,0 +1,31 @@
import SwiftUI
extension ChartGrid {
public func setNumberOfHorizontalLines(_ numberOfLines: Int) -> ChartGrid {
self.gridOptions.numberOfHorizontalLines = numberOfLines
return self
}
public func setNumberOfVerticalLines(_ numberOfLines: Int) -> ChartGrid {
self.gridOptions.numberOfVerticalLines = numberOfLines
return self
}
public func setStoreStyle(_ strokeStyle: StrokeStyle) -> ChartGrid {
self.gridOptions.strokeStyle = strokeStyle
return self
}
public func setColor(_ color: Color) -> ChartGrid {
self.gridOptions.color = color
return self
}
public func showBaseLine(_ show: Bool, with style: StrokeStyle? = nil) -> ChartGrid {
self.gridOptions.showBaseLine = show
if let style = style {
self.gridOptions.baseStrokeStyle = style
}
return self
}
}
@@ -0,0 +1,14 @@
import SwiftUI
public final class GridOptions: ObservableObject {
@Published public var numberOfHorizontalLines: Int = 3
@Published public var numberOfVerticalLines: Int = 3
@Published public var strokeStyle: StrokeStyle = StrokeStyle(lineWidth: 1, lineCap: .round, dash: [5, 10])
@Published public var color: Color = Color(white: 0.85)
@Published public var showBaseLine: Bool = true
@Published public var baseStrokeStyle: StrokeStyle = StrokeStyle(lineWidth: 1.5, lineCap: .round, dash: [5, 0])
public init() {
// no-op
}
}
@@ -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: 0, bottom: 0.0, trailing: 8.0)
case .legend:
return EdgeInsets(top: 4.0, leading: 0, bottom: 0.0, trailing: 8.0)
case .subTitle:
return EdgeInsets(top: 8.0, leading: 0, bottom: 0.0, trailing: 8.0)
case .largeTitle:
return EdgeInsets(top: 24.0, leading: 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.primary
case .legend:
return Color.secondary
case .subTitle:
return Color.primary
case .largeTitle:
return Color.primary
case .custom(_, _, let color):
return color
}
}
/// Initialize
/// - Parameters:
/// - title: Any `String`
/// - type: Which `ChartLabelType` to use
public init (_ title: String,
type: ChartLabelType = .title,
format: String = "%.01f") {
self.title = title
labelType = type
self.format = format
}
/// The content and behavior of the `ChartLabel`.
///
/// Displays current value if chart is currently being touched along a data point, otherwise the specified text.
public var body: some View {
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,27 @@
import SwiftUI
public class ChartStyle: ObservableObject {
public let backgroundColor: ColorGradient
public let foregroundColor: [ColorGradient]
public init(backgroundColor: Color, foregroundColor: [ColorGradient]) {
self.backgroundColor = ColorGradient.init(backgroundColor)
self.foregroundColor = foregroundColor
}
public init(backgroundColor: Color, foregroundColor: ColorGradient) {
self.backgroundColor = ColorGradient.init(backgroundColor)
self.foregroundColor = [foregroundColor]
}
public init(backgroundColor: ColorGradient, foregroundColor: ColorGradient) {
self.backgroundColor = backgroundColor
self.foregroundColor = [foregroundColor]
}
public init(backgroundColor: ColorGradient, foregroundColor: [ColorGradient]) {
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
}
}
@@ -0,0 +1,33 @@
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 {
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,9 @@
import SwiftUI
public enum ChartColors {
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")
}
-33
View File
@@ -1,33 +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
@State var scaleValue: Double = 0
public var body: some View {
Rectangle()
.frame(width: 6)
.cornerRadius(4)
.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))
}
}
#endif
-31
View File
@@ -1,31 +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 {
HStack(alignment: .bottom, spacing: 14){
ForEach(0..<data.count) { i in
ChartCell(value: Double(self.data[i])/Double(self.maxValue), index: i)
}
}.padding([.trailing,.leading])
}
}
#if DEBUG
struct ChartRow_Previews : PreviewProvider {
static var previews: some View {
ChartRow(data: [8,23,54,32,12,37,7])
}
}
#endif
-59
View File
@@ -1,59 +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)
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,13 @@
import SwiftUI
public struct BarChart: ChartBase {
public var chartData = ChartData()
@EnvironmentObject var style: ChartStyle
public var body: some View {
BarChartRow(chartData: chartData, style: style)
}
public init() {}
}
@@ -0,0 +1,52 @@
import SwiftUI
public struct BarChartCell: View {
var value: Double
var index: Int = 0
var gradientColor: ColorGradient
var touchLocation: CGFloat
@State private var didCellAppear: Bool = false
public init( value: Double,
index: Int = 0,
gradientColor: ColorGradient,
touchLocation: CGFloat) {
self.value = value
self.index = index
self.gradientColor = gradientColor
self.touchLocation = touchLocation
}
public var body: some View {
BarChartCellShape(value: didCellAppear ? value : 0.0)
.fill(gradientColor.linearGradient(from: .bottom, to: .top)) .onAppear {
self.didCellAppear = true
}
.onDisappear {
self.didCellAppear = false
}
.transition(.slide)
.animation(Animation.spring().delay(self.touchLocation < 0 || !didCellAppear ? Double(self.index) * 0.04 : 0))
}
}
struct BarChartCell_Previews: PreviewProvider {
static var previews: some View {
Group {
Group {
BarChartCell(value: 0, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
BarChartCell(value: 0.5, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
BarChartCell(value: 0.75, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat())
BarChartCell(value: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat())
}
Group {
BarChartCell(value: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
BarChartCell(value: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat())
BarChartCell(value: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat())
}.environment(\.colorScheme, .dark)
}
}
}
@@ -0,0 +1,49 @@
import SwiftUI
struct BarChartCellShape: Shape, Animatable {
var value: Double
var cornerRadius: CGFloat = 6.0
var animatableData: CGFloat {
get { CGFloat(value) }
set { value = Double(newValue) }
}
func path(in rect: CGRect) -> Path {
let adjustedOriginY = rect.height - (rect.height * CGFloat(value))
var path = Path()
path.move(to: CGPoint(x: 0.0 , y: rect.height))
path.addLine(to: CGPoint(x: 0.0, y: adjustedOriginY + cornerRadius))
path.addArc(center: CGPoint(x: cornerRadius, y: adjustedOriginY + cornerRadius),
radius: cornerRadius,
startAngle: Angle(radians: Double.pi),
endAngle: Angle(radians: value < 0 ? Double.pi/2 : -Double.pi/2),
clockwise: value < 0 ? true : false)
path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: value < 0 ? adjustedOriginY + 2 * cornerRadius : adjustedOriginY))
path.addArc(center: CGPoint(x: rect.width - cornerRadius, y: adjustedOriginY + cornerRadius),
radius: cornerRadius,
startAngle: Angle(radians: value < 0 ? Double.pi/2 : -Double.pi/2),
endAngle: Angle(radians: 0),
clockwise: value < 0 ? true : false)
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.closeSubpath()
return path
}
}
struct BarChartCellShape_Previews: PreviewProvider {
static var previews: some View {
Group {
BarChartCellShape(value: 0.75)
.fill(Color.red)
BarChartCellShape(value: 0.3)
.fill(Color.blue)
BarChartCellShape(value: -0.3)
.fill(Color.blue)
.offset(x: 0, y: -600)
}
}
}
@@ -0,0 +1,70 @@
import SwiftUI
public struct BarChartRow: View {
@EnvironmentObject var chartValue: ChartValue
@ObservedObject var chartData: ChartData
@State private var touchLocation: CGFloat = -1.0
var style: ChartStyle
var maxValue: Double {
guard let max = chartData.points.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 / CGFloat(chartData.data.count * 3)) {
ForEach(0..<chartData.data.count, id: \.self) { index in
BarChartCell(value: chartData.normalisedPoints[index],
index: index,
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()
}
.frame(maxHeight: chartData.isInNegativeDomain ? geometry.size.height / 2 : geometry.size.height)
.gesture(DragGesture()
.onChanged({ value in
let width = geometry.frame(in: .local).width
self.touchLocation = value.location.x/width
if let currentValue = self.getCurrentValue(width: width) {
self.chartValue.currentValue = currentValue
self.chartValue.interactionInProgress = true
}
})
.onEnded({ value in
self.chartValue.interactionInProgress = false
self.touchLocation = -1
})
)
}
}
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)
}
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]
}
}
struct BarChartRow_Previews: PreviewProvider {
static let chartData = ChartData([6, 2, 5, 8, 6])
static let chartStyle = ChartStyle(backgroundColor: .white, foregroundColor: .orangeBright)
static var previews: some View {
BarChartRow(chartData: chartData, style: chartStyle)
}
}
@@ -0,0 +1,29 @@
import SwiftUI
extension LineChart {
public func setLineWidth(width: CGFloat) -> LineChart {
self.chartProperties.lineWidth = width
return self
}
public func setBackground(colorGradient: ColorGradient) -> LineChart {
self.chartProperties.backgroundGradient = colorGradient
return self
}
public func showChartMarks(_ show: Bool, with color: ColorGradient? = nil) -> LineChart {
self.chartProperties.showChartMarks = show
self.chartProperties.customChartMarksColors = color
return self
}
public func setLineStyle(to style: LineStyle) -> LineChart {
self.chartProperties.lineStyle = style
return self
}
public func withAnimation(_ enabled: Bool) -> LineChart {
self.chartProperties.animationEnabled = enabled
return self
}
}
@@ -0,0 +1,20 @@
import SwiftUI
struct IndicatorPoint: View {
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,111 @@
import SwiftUI
/// A single line of data, a view in a `LineChart`
public struct Line: View {
@ObservedObject var chartData: ChartData
@ObservedObject var chartProperties: LineChartProperties
var curvedLines: Bool = true
var style: ChartStyle
@State private var showIndicator: Bool = false
@State private var touchLocation: CGPoint = .zero
@State private var didCellAppear: Bool = false
var path: Path {
Path.quadCurvedPathWithPoints(points: chartData.normalisedPoints,
step: CGPoint(x: 1.0, y: 1.0))
}
public init(chartData: ChartData,
style: ChartStyle,
chartProperties: LineChartProperties) {
self.chartData = chartData
self.style = style
self.chartProperties = chartProperties
}
public var body: some View {
GeometryReader { geometry in
ZStack {
if self.didCellAppear, let backgroundColor = chartProperties.backgroundGradient {
LineBackgroundShapeView(chartData: chartData,
geometry: geometry,
backgroundColor: backgroundColor)
}
lineShapeView(geometry: geometry)
}
.onAppear {
didCellAppear = true
}
.onDisappear() {
didCellAppear = false
}
}
}
@ViewBuilder
private func lineShapeView(geometry: GeometryProxy) -> some View {
if chartProperties.animationEnabled {
LineShapeView(chartData: chartData,
chartProperties: chartProperties,
geometry: geometry,
style: style,
trimTo: didCellAppear ? 1.0 : 0.0)
.animation(Animation.easeIn(duration: 0.75))
} else {
LineShapeView(chartData: chartData,
chartProperties: chartProperties,
geometry: geometry,
style: style,
trimTo: 1.0)
}
}
}
// 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(geometry: GeometryProxy, touchLocation: CGPoint) -> CGPoint {
let geometryWidth = geometry.frame(in: .local).width
let normalisedTouchLocationX = (touchLocation.x / geometryWidth) * CGFloat(chartData.normalisedPoints.count - 1)
let closest = self.path.point(to: normalisedTouchLocationX)
var denormClosest = closest.denormalize(with: geometry)
denormClosest.x = denormClosest.x / CGFloat(chartData.normalisedPoints.count - 1)
denormClosest.y = denormClosest.y / CGFloat(chartData.normalisedYRange)
return denormClosest
}
// /// Figure out where closest touch point was
// /// - Parameter point: location of data point on graph, near touch location
private func getClosestDataPoint(geometry: GeometryProxy, touchLocation: CGPoint) {
let geometryWidth = geometry.frame(in: .local).width
let index = Int(round((touchLocation.x / geometryWidth) * CGFloat(chartData.points.count - 1)))
if (index >= 0 && index < self.chartData.data.count){
// self.chartValue.currentValue = self.chartData.points[index]
}
}
}
struct Line_Previews: PreviewProvider {
/// Predefined style, black over white, for preview
static let blackLineStyle = ChartStyle(backgroundColor: ColorGradient(.white), foregroundColor: ColorGradient(.black))
/// Predefined style red over white, for preview
static let redLineStyle = ChartStyle(backgroundColor: .whiteBlack, foregroundColor: ColorGradient(.red))
static var previews: some View {
Group {
Line(chartData: ChartData([8, 23, 32, 7, 23, -4]),
style: blackLineStyle,
chartProperties: LineChartProperties())
Line(chartData: ChartData([8, 23, 32, 7, 23, 43]),
style: redLineStyle,
chartProperties: LineChartProperties())
}
}
}
@@ -0,0 +1,27 @@
import SwiftUI
struct LineBackgroundShape: Shape {
var data: [(Double, Double)]
func path(in rect: CGRect) -> Path {
let path = Path.quadClosedCurvedPathWithPoints(data: data, in: rect)
return path
}
}
struct LineBackgroundShape_Previews: PreviewProvider {
static var previews: some View {
Group {
GeometryReader { geometry in
LineBackgroundShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)])
.fill(Color.red)
.toStandardCoordinateSystem()
}
GeometryReader { geometry in
LineBackgroundShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)])
.fill(Color.blue)
.toStandardCoordinateSystem()
}
}
}
}
@@ -0,0 +1,16 @@
import SwiftUI
struct LineBackgroundShapeView: View {
var chartData: ChartData
var geometry: GeometryProxy
var backgroundColor: ColorGradient
var body: some View {
LineBackgroundShape(data: chartData.normalisedData)
.fill(LinearGradient(gradient: Gradient(colors: [backgroundColor.startColor,
backgroundColor.endColor]),
startPoint: .bottom,
endPoint: .top))
.toStandardCoordinateSystem()
}
}
@@ -0,0 +1,15 @@
import SwiftUI
public struct LineChart: ChartBase {
public var chartData = ChartData()
@EnvironmentObject var style: ChartStyle
public var chartProperties = LineChartProperties()
public var body: some View {
Line(chartData: chartData,
style: style,
chartProperties: chartProperties)
}
public init() {}
}
@@ -0,0 +1,30 @@
import SwiftUI
struct LineShape: Shape {
var data: [(Double, Double)]
var lineStyle: LineStyle = .curved
func path(in rect: CGRect) -> Path {
var path = Path()
switch lineStyle {
case .curved:
path = Path.quadCurvedPathWithPoints(data: data, in: rect)
case .straight:
path = Path.linePathWithPoints(data: data, in: rect)
}
return path
}
}
struct LineShape_Previews: PreviewProvider {
static var previews: some View {
Group {
LineShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)])
.stroke()
.toStandardCoordinateSystem()
LineShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)], lineStyle: .straight)
.stroke()
.toStandardCoordinateSystem()
}
}
}
@@ -0,0 +1,83 @@
import SwiftUI
struct LineShapeView: View, Animatable {
var chartData: ChartData
var chartProperties: LineChartProperties
var geometry: GeometryProxy
var style: ChartStyle
var trimTo: Double = 0
var animatableData: CGFloat {
get { CGFloat(trimTo) }
set { trimTo = Double(newValue) }
}
var chartMarkColor: LinearGradient {
if let customColor = chartProperties.customChartMarksColors {
return customColor.linearGradient(from: .leading, to: .trailing)
}
return LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient,
startPoint: .leading,
endPoint: .trailing)
}
var body: some View {
ZStack {
LineShape(data: chartData.normalisedData, lineStyle: chartProperties.lineStyle)
.trim(from: 0, to: CGFloat(trimTo))
.stroke(LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient,
startPoint: .leading,
endPoint: .trailing),
style: StrokeStyle(lineWidth: chartProperties.lineWidth, lineJoin: .round))
.toStandardCoordinateSystem()
.clipped()
if chartProperties.showChartMarks {
MarkerShape(data: chartData.normalisedData)
.trim(from: 0, to: CGFloat(trimTo))
.fill(.white,
strokeBorder: chartMarkColor,
lineWidth: chartProperties.lineWidth)
.toStandardCoordinateSystem()
}
}
}
}
struct LineShapeView_Previews: PreviewProvider {
static let chartData = ChartData([6, 8, 6], rangeY: 6...10)
static let chartDataOutOfRange = ChartData([-1, 8, 6, 12, 3], rangeY: -5...15)
static let chartDataOutOfRange2 = ChartData([6,6,8,5], rangeY: 5...10)
static let chartStyle = ChartStyle(backgroundColor: Color.white,
foregroundColor: [ColorGradient(Color.orange, Color.red)])
static var previews: some View {
Group {
GeometryReader { geometry in
LineShapeView(chartData: chartData,
chartProperties: LineChartProperties(),
geometry: geometry,
style: chartStyle,
trimTo: 1.0)
}
GeometryReader { geometry in
LineShapeView(chartData: chartDataOutOfRange,
chartProperties: LineChartProperties(),
geometry: geometry,
style: chartStyle,
trimTo: 1.0)
}
GeometryReader { geometry in
LineShapeView(chartData: chartDataOutOfRange2,
chartProperties: LineChartProperties(),
geometry: geometry,
style: chartStyle,
trimTo: 1.0)
}
}
}
}
@@ -0,0 +1,23 @@
import SwiftUI
struct MarkerShape: Shape {
var data: [(Double, Double)]
func path(in rect: CGRect) -> Path {
let path = Path.drawChartMarkers(data: data, in: rect)
return path
}
}
struct MarkerShape_Previews: PreviewProvider {
static var previews: some View {
Group {
MarkerShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)])
.stroke()
.toStandardCoordinateSystem()
MarkerShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)])
.stroke()
.toStandardCoordinateSystem()
}
}
}
@@ -0,0 +1,13 @@
import SwiftUI
public class LineChartProperties: ObservableObject {
@Published var lineWidth: CGFloat = 2.0
@Published var backgroundGradient: ColorGradient?
@Published var showChartMarks: Bool = true
@Published var customChartMarksColors: ColorGradient?
@Published var lineStyle: LineStyle = .curved
@Published var animationEnabled: Bool = true
public init() {
// no-op
}
}
@@ -0,0 +1,6 @@
import Foundation
public enum LineStyle {
case curved
case straight
}
@@ -0,0 +1,17 @@
import SwiftUI
/// A type of chart that displays a slice of "pie" for each data point
public struct PieChart: ChartBase {
public var chartData = ChartData()
@EnvironmentObject var style: ChartStyle
/// The content and behavior of the `PieChart`.
///
///
public var body: some View {
PieChartRow(chartData: chartData, style: style)
}
public init() {}
}
@@ -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,15 @@
import SwiftUI
public struct RingsChart: ChartBase {
public var chartData = ChartData()
@EnvironmentObject var style: ChartStyle
// TODO - should put background opacity, ring width & spacing as chart style values
public var body: some View {
RingsChartRow(width:10.0, spacing:5.0, chartData: chartData, style: style)
}
public init() {}
}
@@ -0,0 +1,133 @@
//
// RingsChartRow.swift
// ChartViewV2Demo
//
// Created by Dan Wood on 8/20/20.
//
import SwiftUI
public struct RingsChartRow: View {
var width : CGFloat
var spacing : CGFloat
@EnvironmentObject var chartValue: ChartValue
@ObservedObject var chartData: ChartData
@State var touchRadius: CGFloat = -1.0
var style: ChartStyle
public var body: some View {
GeometryReader { geometry in
ZStack {
// FIXME: Why is background circle offset strangely when frame isn't specified? See Preview below. Related to the .animation somehow ????
Circle()
.fill(RadialGradient(gradient: self.style.backgroundColor.gradient, center: .center, startRadius: min(geometry.size.width, geometry.size.height)/2.0, endRadius: 1.0))
ForEach(0..<self.chartData.data.count, id: \.self) { index in
let scaleUp = isRingScaled(size:geometry.size, touchRadius: self.touchRadius, index: index)
let scaledWidth = scaleUp ? self.width * 2.0 : self.width
let normalPadding = (width + spacing) * CGFloat(index)
let scaledDiff = (scaledWidth - width) / 2.0
let padding = min(normalPadding - scaledDiff,
min(geometry.size.width, geometry.size.height)/2.0 - width
// make sure it doesn't get to crazy value
)
Ring(ringWidth:scaledWidth, percent: self.chartData.points[index], foregroundColor:self.style.foregroundColor.rotate(for: index),
touchLocation: self.touchRadius)
.zIndex(scaleUp ? 1 : 0) // make sure zoomed one is on top
.padding(padding)
.animation(Animation.easeIn(duration: 0.5))
}
// .drawingGroup()
}
.gesture(DragGesture()
.onChanged({ value in
let frame = geometry.frame(in: .local)
let radius = min(frame.width, frame.height) / 2.0
let deltaX = value.location.x - frame.midX
let deltaY = value.location.y - frame.midY
self.touchRadius = sqrt(deltaX*deltaX + deltaY*deltaY) // Pythagorean equation
if let currentValue = self.getCurrentValue(maxRadius: radius) {
self.chartValue.currentValue = currentValue
self.chartValue.interactionInProgress = true
}
})
.onEnded({ value in
self.chartValue.interactionInProgress = false
self.touchRadius = -1
})
)
}
}
/// should we scale up the touched ring?
/// - Parameters:
/// - size: size of the view
/// - touchRadius: distance from center where touched
/// - index: which ring is being drawn
/// - Returns: size to scale up or just scale of 1 if not scaled up
func isRingScaled(size: CGSize, touchRadius: CGFloat, index: Int) -> Bool {
let radius = min(size.width, size.height) / 2.0
return index == self.touchedCircleIndex(maxRadius: radius)
}
/// Find which circle has been touched
/// - Parameter maxRadius: radius of overall view circle
/// - Returns: which circle index was touched, if found. 0 = outer, 1 = next one in, etc.
func touchedCircleIndex(maxRadius: CGFloat) -> Int? {
guard self.chartData.data.count > 0 else { return nil } // no data
// Pretend actual circle goes ½ the inter-ring spacing out, so that a touch
// is registered on either side of each ring
let radialDistanceFromEdge = (maxRadius + spacing/2) - self.touchRadius;
guard radialDistanceFromEdge >= 0 else { return nil } // touched outside of ring
let touchIndex = Int(floor(radialDistanceFromEdge / (width + spacing)))
if touchIndex >= self.chartData.data.count { return nil } // too far from outside, no ring
return touchIndex
}
/// Description
/// - Parameter maxRadius: radius of overall view circle
/// - Returns: percentage value of the touched circle, based on `touchRadius` if found
func getCurrentValue(maxRadius: CGFloat) -> Double? {
guard let index = self.touchedCircleIndex(maxRadius: maxRadius) else { return nil }
return self.chartData.points[index]
}
}
struct RingsChartRow_Previews: PreviewProvider {
static var previews: some View {
let multiStyle = ChartStyle(backgroundColor: ColorGradient(Color.black.opacity(0.05), Color.white),
foregroundColor:
[ColorGradient(.purple, .blue),
ColorGradient(.orange, .red),
ColorGradient(.green, .yellow),
])
return RingsChartRow(width:20.0, spacing:10.0, chartData: ChartData([25,50,75,100,125]), style: multiStyle)
// and why does this not get centered when frame isn't specified?
.frame(width:300, height:400)
}
}
-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,37 @@
@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