Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 40f9210205 | |||
| 57ac969092 | |||
| dff16e8d2d | |||
| f0eea58bd8 | |||
| d64d0e9d7a | |||
| 0caebce9ff | |||
| f2866ae281 | |||
| 4963ec54ed | |||
| 99b952fcf4 | |||
| b5beab55e6 | |||
| aa9126482f | |||
| a2d75dca0e | |||
| 5cd5858967 | |||
| d869e4186f |
@@ -1,34 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!--- Provide a general summary of the issue in the Title above -->
|
||||
|
||||
|
||||
|
||||
## Description
|
||||
<!--- Provide a more detailed introduction to the issue itself, and why you consider it to be a bug -->
|
||||
|
||||
## Expected Behavior
|
||||
<!--- Tell us what should happen -->
|
||||
|
||||
## Actual Behavior
|
||||
<!--- Tell us what happens instead -->
|
||||
|
||||
## Possible Fix
|
||||
<!--- Not obligatory, but suggest a fix or reason for the bug -->
|
||||
|
||||
## Steps to Reproduce
|
||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
||||
|
||||
## Your Environment
|
||||
<!--- Include as many relevant details about the environment -->
|
||||
* Version of this package used:
|
||||
* Device/Simulator:
|
||||
* Operating System and version:
|
||||
* Link to your project:
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Ask for a new feature
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--- Provide a general summary of the issue in the Title above -->
|
||||
|
||||
## Detailed Description
|
||||
<!--- Provide a detailed description of the change or addition you are proposing -->
|
||||
|
||||
## Context
|
||||
<!--- Why is this change important to you? How would you use it? -->
|
||||
<!--- How can it benefit other users? -->
|
||||
|
||||
## Possible Implementation
|
||||
<!--- Not obligatory, but suggest an idea for implementing addition or change -->
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
name: v2 ticket
|
||||
about: Create tasks for the upcoming new version
|
||||
title: ''
|
||||
labels: v2
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
# v2 ticket
|
||||
|
||||
## Ticket description:
|
||||
@@ -1,29 +0,0 @@
|
||||
<!--- Provide a general summary of your changes in the Title above -->
|
||||
|
||||
## Description
|
||||
<!--- Describe your changes in detail -->
|
||||
|
||||
## Motivation and Context
|
||||
<!--- Why is this change required? What problem does it solve? -->
|
||||
<!--- If it fixes an open issue, please link to the issue here. -->
|
||||
|
||||
## How Has This Been Tested?
|
||||
<!--- Please describe in detail how you tested your changes. -->
|
||||
<!--- Include details of your testing environment, and the tests you ran to -->
|
||||
<!--- see how your change affects other areas of the code, etc. -->
|
||||
|
||||
## Screenshots (if appropriate):
|
||||
|
||||
## Types of changes
|
||||
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
- [ ] Non-functional change (Updating Documentation, CI automation, etc..)
|
||||
|
||||
## Checklist:
|
||||
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
||||
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
||||
- [ ] My code follows the code style of this project.
|
||||
- [ ] My change requires a change to the documentation.
|
||||
- [ ] I have updated the documentation accordingly.
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Swift
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- new-version
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- new-version
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: swift build -v
|
||||
- name: Run tests
|
||||
run: swift test -v
|
||||
@@ -2,4 +2,3 @@
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
.swiftpm
|
||||
|
||||
@@ -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"
|
||||
@@ -7,7 +7,7 @@
|
||||
<key>SwiftUICharts.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// swift-tools-version:5.3
|
||||
// swift-tools-version:5.1
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
@@ -6,13 +6,13 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "SwiftUICharts",
|
||||
platforms: [
|
||||
.iOS(.v13), .watchOS(.v6), .macOS(.v11)
|
||||
.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"])
|
||||
]
|
||||
)
|
||||
|
||||
@@ -2,43 +2,18 @@
|
||||
|
||||
Swift package for displaying charts effortlessly.
|
||||
|
||||
## V2 Beta is here 🎉🎉🎉
|
||||
|
||||
V2 focuses on providing a strong and easy to use base, on which you can build your beautiful custom charts. It provides basic building blocks, like a chart view (bar, pie, line and ring chart), grid view, card view, interactive label for displaying the curent chart value.
|
||||
So you decide, whether you build a fully fledged interactive view, or just display a bare bone chart
|
||||
|
||||
### It supports interactions and animations
|
||||
<img src="https://user-images.githubusercontent.com/2826764/130787802-9aa619ee-05de-4343-ba3c-1796e4d05e08.gif" width="26%"></img> <img src="https://user-images.githubusercontent.com/2826764/130787814-283f3d26-6c9d-448b-b2c7-879e60a3b05d.gif" width="26%"></img>
|
||||
|
||||
### It is fully customizable, and works together with native SwiftUI elements well
|
||||
<img src="https://user-images.githubusercontent.com/2826764/130785262-010d6791-16cf-485d-b920-29e4086477e2.png" width="45%"></img> <img src="https://user-images.githubusercontent.com/2826764/130785266-94a08622-2963-4177-8777-8bd3ad463809.png" width="45%"></img> <img src="https://user-images.githubusercontent.com/2826764/130785268-284314de-ba96-4fb7-a1e5-8a46578e1f0e.png" width="45%"></img>
|
||||
|
||||
### Installation
|
||||
please check out `2.0.0-beta.2` tag
|
||||
|
||||

|
||||
|
||||
## Original (stable) version:
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2826764/131211993-5d33312b-09af-44b4-a32e-ffaad739adfe.gif" width="45%"></img> <img src="https://user-images.githubusercontent.com/2826764/131211994-48c9ce4e-2e67-40a0-b727-c88bdbd22cd0.gif" width="45%">
|
||||
|
||||
### Usage
|
||||

|
||||
|
||||
It supports:
|
||||
* Line charts
|
||||
* Bar charts
|
||||
* Pie charts
|
||||
|
||||
### Slack
|
||||
Join our Slack channel for day to day conversation and more insights:
|
||||
|
||||
[Slack invite link](https://join.slack.com/t/swiftuichartview/shared_invite/zt-g6mxioq8-j3iUTF1YKX7D23ML3qcc4g)
|
||||
|
||||
### Installation:
|
||||
|
||||
It requires iOS 13 and Xcode 11!
|
||||
|
||||
In Xcode go to `File -> Swift Packages -> Add Package Dependency` and paste in the repo's url: `https://github.com/AppPear/ChartView`
|
||||
In Xcode got to `File -> Swift Packages -> Add Package Dependency` and paste inthe repo's url: `https://github.com/AppPear/ChartView`
|
||||
|
||||
### Usage:
|
||||
|
||||
@@ -54,8 +29,7 @@ Added an example project, with **iOS, watchOS** target: https://github.com/AppPe
|
||||
|
||||
**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.
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2826764/131211991-eca64276-cf05-423f-a78a-697c55e44bbc.gif" width="50%"></img>
|
||||

|
||||
|
||||
Usage:
|
||||
```swift
|
||||
@@ -76,6 +50,7 @@ Available preset gradients:
|
||||
|
||||
**Full screen view called LineView!!!**
|
||||
|
||||

|
||||
|
||||
```swift
|
||||
LineView(data: [8,23,54,32,12,37,7,23,43], title: "Line chart", legend: "Full screen") // legend is optional, use optional .padding()
|
||||
@@ -83,7 +58,7 @@ Available preset gradients:
|
||||
|
||||
Adopts to dark mode automatically
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2826764/131211977-27439357-491d-4872-a6bd-f696edac4c7f.gif" width="45%"></img> <img src="https://user-images.githubusercontent.com/2826764/131211985-f77464d6-7fd8-429d-9e77-9f9bc7424d32.gif" width="45%">
|
||||

|
||||
|
||||
You can add your custom darkmode style by specifying:
|
||||
|
||||
@@ -105,7 +80,7 @@ You can add a line chart with the following code:
|
||||
|
||||
|
||||
## Bar charts
|
||||
<img src="https://user-images.githubusercontent.com/2826764/131211994-48c9ce4e-2e67-40a0-b727-c88bdbd22cd0.gif" width="45%">
|
||||

|
||||
|
||||
**[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**
|
||||
@@ -183,9 +158,10 @@ You can access built-in styles:
|
||||
* barChartMidnightGreenLight
|
||||
* barChartMidnightGreenDark
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2826764/131211990-e41cec90-38f4-4965-8bdc-41c30b79acea.gif" width="45%">
|
||||

|
||||
|
||||

|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2826764/131211999-6ec4f13b-0465-4135-b576-76e31b11a2c6.png" width="45%">
|
||||
|
||||
### You can customize the size of the chart with a ChartForm object:
|
||||
|
||||
@@ -199,16 +175,12 @@ You can access built-in styles:
|
||||
BarChartView(data: [8,23,54,32,12,37,7,23,43], title: "Title", form: ChartForm.small)
|
||||
```
|
||||
|
||||
### You can choose whether bar is animated or not after completing your gesture.
|
||||
|
||||
If you want to animate back movement after completing your gesture, you set `animatedToBack` as `true`.
|
||||
|
||||
### WatchOS support for Bar charts:
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2826764/131212000-a058fdd9-af40-4e64-adc3-82201ea2484d.png" width="45%">
|
||||

|
||||
|
||||
## Pie charts
|
||||
<img src="https://user-images.githubusercontent.com/2826764/131211998-e142657d-0ebc-43b7-aeda-07cae4d9e34b.png" width="45%">
|
||||

|
||||
|
||||
You can add a pie chart with the following code:
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 514 KiB |
|
After Width: | Height: | Size: 502 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 343 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 73 KiB |
@@ -1,44 +0,0 @@
|
||||
//
|
||||
// ChartCell.swift
|
||||
// ChartView
|
||||
//
|
||||
// Created by András Samu on 2019. 06. 12..
|
||||
// Copyright © 2019. András Samu. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct BarChartCell : View {
|
||||
var value: Double
|
||||
var index: Int = 0
|
||||
var width: Float
|
||||
var numberOfDataPoints: Int
|
||||
var cellWidth: Double {
|
||||
return Double(width)/(Double(numberOfDataPoints) * 1.5)
|
||||
}
|
||||
var accentColor: Color
|
||||
var gradient: GradientColor?
|
||||
|
||||
@State var scaleValue: Double = 0
|
||||
@Binding var touchLocation: CGFloat
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(LinearGradient(gradient: gradient?.getGradient() ?? GradientColor(start: accentColor, end: accentColor).getGradient(), startPoint: .bottom, endPoint: .top))
|
||||
}
|
||||
.frame(width: CGFloat(self.cellWidth))
|
||||
.scaleEffect(CGSize(width: 1, height: self.scaleValue), anchor: .bottom)
|
||||
.onAppear(){
|
||||
self.scaleValue = self.value
|
||||
}
|
||||
.animation(Animation.spring().delay(self.touchLocation < 0 ? Double(self.index) * 0.04 : 0))
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct ChartCell_Previews : PreviewProvider {
|
||||
static var previews: some View {
|
||||
BarChartCell(value: Double(0.75), width: 320, numberOfDataPoints: 12, accentColor: Colors.OrangeStart, gradient: nil, touchLocation: .constant(-1))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,57 +0,0 @@
|
||||
//
|
||||
// ChartRow.swift
|
||||
// ChartView
|
||||
//
|
||||
// Created by András Samu on 2019. 06. 12..
|
||||
// Copyright © 2019. András Samu. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct BarChartRow : View {
|
||||
var data: [Double]
|
||||
var accentColor: Color
|
||||
var gradient: GradientColor?
|
||||
|
||||
var maxValue: Double {
|
||||
guard let max = data.max() else {
|
||||
return 1
|
||||
}
|
||||
return max != 0 ? max : 1
|
||||
}
|
||||
@Binding var touchLocation: CGFloat
|
||||
public var body: some View {
|
||||
GeometryReader { geometry in
|
||||
HStack(alignment: .bottom, spacing: (geometry.frame(in: .local).width-22)/CGFloat(self.data.count * 3)){
|
||||
ForEach(0..<self.data.count, id: \.self) { i in
|
||||
BarChartCell(value: self.normalizedValue(index: i),
|
||||
index: i,
|
||||
width: Float(geometry.frame(in: .local).width - 22),
|
||||
numberOfDataPoints: self.data.count,
|
||||
accentColor: self.accentColor,
|
||||
gradient: self.gradient,
|
||||
touchLocation: self.$touchLocation)
|
||||
.scaleEffect(self.touchLocation > CGFloat(i)/CGFloat(self.data.count) && self.touchLocation < CGFloat(i+1)/CGFloat(self.data.count) ? CGSize(width: 1.4, height: 1.1) : CGSize(width: 1, height: 1), anchor: .bottom)
|
||||
.animation(.spring())
|
||||
|
||||
}
|
||||
}
|
||||
.padding([.top, .leading, .trailing], 10)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizedValue(index: Int) -> Double {
|
||||
return Double(self.data[index])/Double(self.maxValue)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct ChartRow_Previews : PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
BarChartRow(data: [0], accentColor: Colors.OrangeStart, touchLocation: .constant(-1))
|
||||
BarChartRow(data: [8,23,54,32,12,37,7], accentColor: Colors.OrangeStart, touchLocation: .constant(-1))
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,160 +0,0 @@
|
||||
//
|
||||
// ChartView.swift
|
||||
// ChartView
|
||||
//
|
||||
// Created by András Samu on 2019. 06. 12..
|
||||
// Copyright © 2019. András Samu. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct BarChartView : View {
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
private var data: ChartData
|
||||
public var title: String
|
||||
public var legend: String?
|
||||
public var style: ChartStyle
|
||||
public var darkModeStyle: ChartStyle
|
||||
public var formSize:CGSize
|
||||
public var dropShadow: Bool
|
||||
public var cornerImage: Image?
|
||||
public var valueSpecifier:String
|
||||
public var animatedToBack: Bool
|
||||
|
||||
@State private var touchLocation: CGFloat = -1.0
|
||||
@State private var showValue: Bool = false
|
||||
@State private var showLabelValue: Bool = false
|
||||
@State private var currentValue: Double = 0 {
|
||||
didSet{
|
||||
if(oldValue != self.currentValue && self.showValue) {
|
||||
HapticFeedback.playSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
var isFullWidth:Bool {
|
||||
return self.formSize == ChartForm.large
|
||||
}
|
||||
public init(data:ChartData, title: String, legend: String? = nil, style: ChartStyle = Styles.barChartStyleOrangeLight, form: CGSize? = ChartForm.medium, dropShadow: Bool? = true, cornerImage:Image? = Image(systemName: "waveform.path.ecg"), valueSpecifier: String? = "%.1f", animatedToBack: Bool = false){
|
||||
self.data = data
|
||||
self.title = title
|
||||
self.legend = legend
|
||||
self.style = style
|
||||
self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.barChartStyleOrangeDark
|
||||
self.formSize = form!
|
||||
self.dropShadow = dropShadow!
|
||||
self.cornerImage = cornerImage
|
||||
self.valueSpecifier = valueSpecifier!
|
||||
self.animatedToBack = animatedToBack
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack{
|
||||
Rectangle()
|
||||
.fill(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor)
|
||||
.cornerRadius(20)
|
||||
.shadow(color: self.style.dropShadowColor, radius: self.dropShadow ? 8 : 0)
|
||||
VStack(alignment: .leading){
|
||||
HStack{
|
||||
if(!showValue){
|
||||
Text(self.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
|
||||
}else{
|
||||
Text("\(self.currentValue, specifier: self.valueSpecifier)")
|
||||
.font(.headline)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
|
||||
}
|
||||
if(self.formSize == ChartForm.large && self.legend != nil && !showValue) {
|
||||
Text(self.legend!)
|
||||
.font(.callout)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.accentColor : self.style.accentColor)
|
||||
.transition(.opacity)
|
||||
.animation(.easeOut)
|
||||
}
|
||||
Spacer()
|
||||
self.cornerImage
|
||||
.imageScale(.large)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor)
|
||||
}.padding()
|
||||
BarChartRow(data: data.points.map{$0.1},
|
||||
accentColor: self.colorScheme == .dark ? self.darkModeStyle.accentColor : self.style.accentColor,
|
||||
gradient: self.colorScheme == .dark ? self.darkModeStyle.gradientColor : self.style.gradientColor,
|
||||
touchLocation: self.$touchLocation)
|
||||
if self.legend != nil && self.formSize == ChartForm.medium && !self.showLabelValue{
|
||||
Text(self.legend!)
|
||||
.font(.headline)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor)
|
||||
.padding()
|
||||
}else if (self.data.valuesGiven && self.getCurrentValue() != nil) {
|
||||
LabelView(arrowOffset: self.getArrowOffset(touchLocation: self.touchLocation),
|
||||
title: .constant(self.getCurrentValue()!.0))
|
||||
.offset(x: self.getLabelViewOffset(touchLocation: self.touchLocation), y: -6)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor)
|
||||
}
|
||||
|
||||
}
|
||||
}.frame(minWidth:self.formSize.width,
|
||||
maxWidth: self.isFullWidth ? .infinity : self.formSize.width,
|
||||
minHeight:self.formSize.height,
|
||||
maxHeight:self.formSize.height)
|
||||
.gesture(DragGesture()
|
||||
.onChanged({ value in
|
||||
self.touchLocation = value.location.x/self.formSize.width
|
||||
self.showValue = true
|
||||
self.currentValue = self.getCurrentValue()?.1 ?? 0
|
||||
if(self.data.valuesGiven && self.formSize == ChartForm.medium) {
|
||||
self.showLabelValue = true
|
||||
}
|
||||
})
|
||||
.onEnded({ value in
|
||||
if animatedToBack {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
withAnimation(Animation.easeOut(duration: 1)) {
|
||||
self.showValue = false
|
||||
self.showLabelValue = false
|
||||
self.touchLocation = -1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.showValue = false
|
||||
self.showLabelValue = false
|
||||
self.touchLocation = -1
|
||||
}
|
||||
})
|
||||
)
|
||||
.gesture(TapGesture()
|
||||
)
|
||||
}
|
||||
|
||||
func getArrowOffset(touchLocation:CGFloat) -> Binding<CGFloat> {
|
||||
let realLoc = (self.touchLocation * self.formSize.width) - 50
|
||||
if realLoc < 10 {
|
||||
return .constant(realLoc - 10)
|
||||
}else if realLoc > self.formSize.width-110 {
|
||||
return .constant((self.formSize.width-110 - realLoc) * -1)
|
||||
} else {
|
||||
return .constant(0)
|
||||
}
|
||||
}
|
||||
|
||||
func getLabelViewOffset(touchLocation:CGFloat) -> CGFloat {
|
||||
return min(self.formSize.width-110,max(10,(self.touchLocation * self.formSize.width) - 50))
|
||||
}
|
||||
|
||||
func getCurrentValue() -> (String,Double)? {
|
||||
guard self.data.points.count > 0 else { return nil}
|
||||
let index = max(0,min(self.data.points.count-1,Int(floor((self.touchLocation*self.formSize.width)/(self.formSize.width/CGFloat(self.data.points.count))))))
|
||||
return self.data.points[index]
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct ChartView_Previews : PreviewProvider {
|
||||
static var previews: some View {
|
||||
BarChartView(data: TestData.values ,
|
||||
title: "Model 3 sales",
|
||||
legend: "Quarterly",
|
||||
valueSpecifier: "%.0f")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,46 +0,0 @@
|
||||
//
|
||||
// LabelView.swift
|
||||
// BarChart
|
||||
//
|
||||
// Created by Samu András on 2020. 01. 08..
|
||||
// Copyright © 2020. Samu András. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LabelView: View {
|
||||
@Binding var arrowOffset: CGFloat
|
||||
@Binding var title:String
|
||||
var body: some View {
|
||||
VStack{
|
||||
ArrowUp().fill(Color.white).frame(width: 20, height: 12, alignment: .center).shadow(color: Color.gray, radius: 8, x: 0, y: 0).offset(x: getArrowOffset(offset:self.arrowOffset), y: 12)
|
||||
ZStack{
|
||||
RoundedRectangle(cornerRadius: 8).frame(width: 100, height: 32, alignment: .center).foregroundColor(Color.white).shadow(radius: 8)
|
||||
Text(self.title).font(.caption).bold()
|
||||
ArrowUp().fill(Color.white).frame(width: 20, height: 12, alignment: .center).zIndex(999).offset(x: getArrowOffset(offset:self.arrowOffset), y: -20)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getArrowOffset(offset: CGFloat) -> CGFloat {
|
||||
return max(-36,min(36, offset))
|
||||
}
|
||||
}
|
||||
|
||||
struct ArrowUp: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
path.move(to: CGPoint(x: 0, y: rect.height))
|
||||
path.addLine(to: CGPoint(x: rect.width/2, y: 0))
|
||||
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
struct LabelView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LabelView(arrowOffset: .constant(0), title: .constant("Tesla model 3"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import SwiftUI
|
||||
|
||||
public struct CardView<Content: View>: View, ChartBase {
|
||||
public var chartData = ChartData()
|
||||
let content: () -> Content
|
||||
|
||||
@EnvironmentObject var style: ChartStyle
|
||||
|
||||
public init(@ViewBuilder content: @escaping () -> Content) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack{
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(Color.white)
|
||||
.shadow(color: Color.gray, radius: 8)
|
||||
VStack {
|
||||
self.content()
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import SwiftUI
|
||||
|
||||
public protocol ChartBase {
|
||||
var chartData: ChartData { get }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import SwiftUI
|
||||
|
||||
public class ChartData: ObservableObject {
|
||||
@Published public var data: [Double] = []
|
||||
|
||||
public init(_ data: [Double]) {
|
||||
self.data = data
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.data = []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import SwiftUI
|
||||
|
||||
public class ChartValue: ObservableObject {
|
||||
@Published var currentValue: Double = 0
|
||||
@Published var interactionInProgress: Bool = false
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import Foundation
|
||||
|
||||
extension Array where Element == ColorGradient {
|
||||
func rotate(for index: Int) -> ColorGradient {
|
||||
if self.isEmpty {
|
||||
return ColorGradient.orangeBright
|
||||
}
|
||||
|
||||
if self.count <= index {
|
||||
return self[index % self.count]
|
||||
}
|
||||
|
||||
return self[index]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import SwiftUI
|
||||
|
||||
extension CGPoint {
|
||||
static func getStep(frame: CGRect, data: [Double]) -> CGPoint {
|
||||
let padding: CGFloat = 30.0
|
||||
|
||||
// stepWidth
|
||||
var stepWidth: CGFloat = 0.0
|
||||
if data.count < 2 {
|
||||
stepWidth = 0.0
|
||||
}
|
||||
stepWidth = frame.size.width / CGFloat(data.count - 1)
|
||||
|
||||
// stepHeight
|
||||
var stepHeight: CGFloat = 0.0
|
||||
|
||||
var min: Double?
|
||||
var max: Double?
|
||||
if let minPoint = data.min(), let maxPoint = data.max(), minPoint != maxPoint {
|
||||
min = minPoint
|
||||
max = maxPoint
|
||||
} else {
|
||||
return .zero
|
||||
}
|
||||
if let min = min, let max = max, min != max {
|
||||
if min <= 0 {
|
||||
stepHeight = (frame.size.height - padding) / CGFloat(max - min)
|
||||
} else {
|
||||
stepHeight = (frame.size.height - padding) / CGFloat(max + min)
|
||||
}
|
||||
}
|
||||
|
||||
return CGPoint(x: stepWidth, y: stepHeight)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
extension CGRect {
|
||||
// Return the coordinate for a rectangle center
|
||||
public var mid: CGPoint {
|
||||
return CGPoint(x: self.midX, y: self.midY)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import SwiftUI
|
||||
|
||||
extension View where Self: ChartBase {
|
||||
public func data(_ data: [Double]) -> some View {
|
||||
chartData.data = data
|
||||
return self
|
||||
.environmentObject(chartData)
|
||||
.environmentObject(ChartValue())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import SwiftUI
|
||||
|
||||
extension Color {
|
||||
init(hexString: String) {
|
||||
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var int = UInt64()
|
||||
Scanner(string: hex).scanHexInt64(&int)
|
||||
let red, green, blue: UInt64
|
||||
switch hex.count {
|
||||
case 3: // RGB (12-bit)
|
||||
(red, green, blue) = ((int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
||||
case 6: // RGB (24-bit)
|
||||
(red, green, blue) = (int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||
case 8: // ARGB (32-bit)
|
||||
// FIXME: I think we need an an alpha value on this one. See link below.
|
||||
// https://stackoverflow.com/a/56874327/4475605
|
||||
(red, green, blue) = (int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
||||
default:
|
||||
(red, green, blue) = (0, 0, 0)
|
||||
}
|
||||
self.init(red: Double(red) / 255, green: Double(green) / 255, blue: Double(blue) / 255)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,3 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by xspyhack on 2020/1/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension Path {
|
||||
@@ -115,76 +108,80 @@ extension Path {
|
||||
return ret
|
||||
}
|
||||
|
||||
static func quadCurvedPathWithPoints(points:[Double], step:CGPoint, globalOffset: Double? = nil) -> Path {
|
||||
static func quadCurvedPathWithPoints(points: [Double], step: CGPoint, globalOffset: Double? = nil) -> Path {
|
||||
var path = Path()
|
||||
if (points.count < 2){
|
||||
if points.count < 2 {
|
||||
return path
|
||||
}
|
||||
let offset = globalOffset ?? points.min()!
|
||||
// guard let offset = points.min() else { return path }
|
||||
var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
|
||||
path.move(to: p1)
|
||||
var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
|
||||
path.move(to: point1)
|
||||
for pointIndex in 1..<points.count {
|
||||
let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
|
||||
let midPoint = CGPoint.midPointForPoints(p1: p1, p2: p2)
|
||||
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p1))
|
||||
path.addQuadCurve(to: p2, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p2))
|
||||
p1 = p2
|
||||
let point2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
|
||||
let midPoint = CGPoint.midPointForPoints(firstPoint: point1, secondPoint: point2)
|
||||
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point1))
|
||||
path.addQuadCurve(to: point2, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point2))
|
||||
point1 = point2
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
static func quadClosedCurvedPathWithPoints(points:[Double], step:CGPoint, globalOffset: Double? = nil) -> Path {
|
||||
static func quadClosedCurvedPathWithPoints(points: [Double], step: CGPoint, globalOffset: Double? = nil) -> Path {
|
||||
var path = Path()
|
||||
if (points.count < 2){
|
||||
if points.count < 2 {
|
||||
return path
|
||||
}
|
||||
let offset = globalOffset ?? points.min()!
|
||||
|
||||
// guard let offset = points.min() else { return path }
|
||||
path.move(to: .zero)
|
||||
var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
|
||||
path.addLine(to: p1)
|
||||
var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
|
||||
path.addLine(to: point1)
|
||||
for pointIndex in 1..<points.count {
|
||||
let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
|
||||
let midPoint = CGPoint.midPointForPoints(p1: p1, p2: p2)
|
||||
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p1))
|
||||
path.addQuadCurve(to: p2, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p2))
|
||||
p1 = p2
|
||||
let point2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
|
||||
let midPoint = CGPoint.midPointForPoints(firstPoint: point1, secondPoint: point2)
|
||||
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point1))
|
||||
path.addQuadCurve(to: point2, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point2))
|
||||
point1 = point2
|
||||
}
|
||||
path.addLine(to: CGPoint(x: p1.x, y: 0))
|
||||
path.addLine(to: CGPoint(x: point1.x, y: 0))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
|
||||
static func linePathWithPoints(points:[Double], step:CGPoint) -> Path {
|
||||
static func linePathWithPoints(points: [Double], step: CGPoint) -> Path {
|
||||
var path = Path()
|
||||
if (points.count < 2){
|
||||
if points.count < 2 {
|
||||
return path
|
||||
}
|
||||
guard let offset = points.min() else { return path }
|
||||
let p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
|
||||
path.move(to: p1)
|
||||
guard let offset = points.min() else {
|
||||
return path
|
||||
}
|
||||
let point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
|
||||
path.move(to: point1)
|
||||
for pointIndex in 1..<points.count {
|
||||
let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
|
||||
path.addLine(to: p2)
|
||||
let point2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
|
||||
path.addLine(to: point2)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
static func closedLinePathWithPoints(points:[Double], step:CGPoint) -> Path {
|
||||
static func closedLinePathWithPoints(points: [Double], step: CGPoint) -> Path {
|
||||
var path = Path()
|
||||
if (points.count < 2){
|
||||
if points.count < 2 {
|
||||
return path
|
||||
}
|
||||
guard let offset = points.min() else { return path }
|
||||
var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
|
||||
path.move(to: p1)
|
||||
for pointIndex in 1..<points.count {
|
||||
p1 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
|
||||
path.addLine(to: p1)
|
||||
guard let offset = points.min() else {
|
||||
return path
|
||||
}
|
||||
path.addLine(to: CGPoint(x: p1.x, y: 0))
|
||||
var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
|
||||
path.move(to: point1)
|
||||
for pointIndex in 1..<points.count {
|
||||
point1 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
|
||||
path.addLine(to: point1)
|
||||
}
|
||||
path.addLine(to: CGPoint(x: point1.x, y: 0))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
@@ -298,8 +295,8 @@ extension CGPoint {
|
||||
}
|
||||
|
||||
func point(to: CGPoint, t: CGFloat, control1: CGPoint, control2: CGPoint) -> CGPoint {
|
||||
let x = CGPoint.value(x: self.x, y: to.x, t: t, c1: control1.x, c2: control2.x)
|
||||
let y = CGPoint.value(x: self.y, y: to.y, t: t, c1: control1.y, c2: control2.x)
|
||||
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)
|
||||
}
|
||||
@@ -313,12 +310,12 @@ extension CGPoint {
|
||||
return value
|
||||
}
|
||||
|
||||
static func value(x: CGFloat, y: CGFloat, t: CGFloat, c1: CGFloat, c2: CGFloat) -> CGFloat {
|
||||
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 * c1
|
||||
value += 3 * (1-t) * pow(t, 2) * c2
|
||||
value += 3 * pow(1-t, 2) * t * control1
|
||||
value += 3 * (1-t) * pow(t, 2) * control2
|
||||
value += pow(t, 3) * y
|
||||
return value
|
||||
}
|
||||
@@ -334,20 +331,21 @@ extension CGPoint {
|
||||
return sqrt((pow(self.x - to.x, 2) + pow(self.y - to.y, 2)))
|
||||
}
|
||||
|
||||
static func midPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint {
|
||||
return CGPoint(x:(p1.x + p2.x) / 2,y: (p1.y + p2.y) / 2)
|
||||
static func midPointForPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGPoint {
|
||||
return CGPoint(
|
||||
x: (firstPoint.x + secondPoint.x) / 2,
|
||||
y: (firstPoint.y + secondPoint.y) / 2)
|
||||
}
|
||||
|
||||
static func controlPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint {
|
||||
var controlPoint = CGPoint.midPointForPoints(p1:p1, p2:p2)
|
||||
let diffY = abs(p2.y - controlPoint.y)
|
||||
static func controlPointForPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGPoint {
|
||||
var controlPoint = CGPoint.midPointForPoints(firstPoint: firstPoint, secondPoint: secondPoint)
|
||||
let diffY = abs(secondPoint.y - controlPoint.y)
|
||||
|
||||
if (p1.y < p2.y){
|
||||
if firstPoint.y < secondPoint.y {
|
||||
controlPoint.y += diffY
|
||||
} else if (p1.y > p2.y) {
|
||||
} else if firstPoint.y > secondPoint.y {
|
||||
controlPoint.y -= diffY
|
||||
}
|
||||
return controlPoint
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
public func chartStyle(_ style: ChartStyle) -> some View {
|
||||
self.environmentObject(style)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import SwiftUI
|
||||
|
||||
public struct ChartGrid<Content: View>: View, ChartBase {
|
||||
public var chartData = ChartData()
|
||||
let content: () -> Content
|
||||
|
||||
@EnvironmentObject var data: ChartData
|
||||
@EnvironmentObject var style: ChartStyle
|
||||
|
||||
public init(@ViewBuilder content: @escaping () -> Content) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack{
|
||||
self.content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import SwiftUI
|
||||
|
||||
public enum ChartLabelType {
|
||||
case title
|
||||
case subTitle
|
||||
case largeTitle
|
||||
case custom(size: CGFloat, padding: EdgeInsets, color: Color)
|
||||
case legend
|
||||
}
|
||||
|
||||
public struct ChartLabel: View {
|
||||
@EnvironmentObject var chartValue: ChartValue
|
||||
@State var textToDisplay:String = ""
|
||||
|
||||
private var title: String
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private var labelPadding: EdgeInsets {
|
||||
switch labelType {
|
||||
case .title:
|
||||
return EdgeInsets(top: 16.0, leading: 8.0, bottom: 0.0, trailing: 8.0)
|
||||
case .legend:
|
||||
return EdgeInsets(top: 4.0, leading: 8.0, bottom: 0.0, trailing: 8.0)
|
||||
case .subTitle:
|
||||
return EdgeInsets(top: 8.0, leading: 8.0, bottom: 0.0, trailing: 8.0)
|
||||
case .largeTitle:
|
||||
return EdgeInsets(top: 24.0, leading: 8.0, bottom: 0.0, trailing: 8.0)
|
||||
case .custom(_, let padding, _):
|
||||
return padding
|
||||
}
|
||||
}
|
||||
|
||||
private let labelType: ChartLabelType
|
||||
|
||||
private var labelColor: Color {
|
||||
switch labelType {
|
||||
case .title:
|
||||
return .black
|
||||
case .legend:
|
||||
return .gray
|
||||
case .subTitle:
|
||||
return .black
|
||||
case .largeTitle:
|
||||
return .black
|
||||
case .custom(_, _, let color):
|
||||
return color
|
||||
}
|
||||
}
|
||||
|
||||
public init (_ title: String,
|
||||
type: ChartLabelType = .title) {
|
||||
self.title = title
|
||||
labelType = type
|
||||
}
|
||||
|
||||
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: "%.01f", self.chartValue.currentValue) : self.title
|
||||
}
|
||||
if !self.chartValue.interactionInProgress {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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,38 @@
|
||||
import SwiftUI
|
||||
|
||||
public struct ColorGradient: Equatable {
|
||||
public let startColor: Color
|
||||
public let endColor: Color
|
||||
|
||||
public init(_ color: Color) {
|
||||
self.startColor = color
|
||||
self.endColor = color
|
||||
}
|
||||
|
||||
public init (_ startColor: Color, _ endColor: Color) {
|
||||
self.startColor = startColor
|
||||
self.endColor = endColor
|
||||
}
|
||||
|
||||
public var gradient: Gradient {
|
||||
return Gradient(colors: [startColor, endColor])
|
||||
}
|
||||
}
|
||||
|
||||
extension ColorGradient {
|
||||
/// Convenience method to return a LinearGradient from the ColorGradient
|
||||
/// - Parameters:
|
||||
/// - startPoint: starting point
|
||||
/// - endPoint: ending point
|
||||
/// - Returns: a Linear gradient
|
||||
public func linearGradient(from startPoint: UnitPoint, to endPoint: UnitPoint) -> LinearGradient {
|
||||
return LinearGradient(gradient: self.gradient, startPoint: startPoint, endPoint: endPoint)
|
||||
}
|
||||
}
|
||||
|
||||
extension ColorGradient {
|
||||
public static let orangeBright = ColorGradient(ChartColors.orangeBright)
|
||||
public static let redBlack = ColorGradient(.red, .black)
|
||||
public static let greenRed = ColorGradient(.green, .red)
|
||||
public static let whiteBlack = ColorGradient(.white, .black)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import SwiftUI
|
||||
|
||||
public enum ChartColors {
|
||||
// Orange
|
||||
static let orangeBright = Color(hexString: "#FF782C")
|
||||
static let orangeDark = Color(hexString: "#EC2301")
|
||||
|
||||
static let legendColor: Color = Color(hexString: "#E8E7EA")
|
||||
static let indicatorKnob: Color = Color(hexString: "#FF57A6")
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import SwiftUI
|
||||
|
||||
public struct BarChart: View, ChartBase {
|
||||
public var chartData = ChartData()
|
||||
|
||||
@EnvironmentObject var data: ChartData
|
||||
@EnvironmentObject var style: ChartStyle
|
||||
|
||||
public var body: some View {
|
||||
BarChartRow(chartData: data, style: style)
|
||||
}
|
||||
|
||||
public init() {}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import SwiftUI
|
||||
|
||||
public struct BarChartCell: View {
|
||||
var value: Double
|
||||
var index: Int = 0
|
||||
var width: Float
|
||||
var numberOfDataPoints: Int
|
||||
var gradientColor: ColorGradient
|
||||
var touchLocation: CGFloat
|
||||
|
||||
var cellWidth: Double {
|
||||
return Double(width)/(Double(numberOfDataPoints) * 1.5)
|
||||
}
|
||||
|
||||
@State var firstDisplay: Bool = true
|
||||
|
||||
public init( value: Double,
|
||||
index: Int = 0,
|
||||
width: Float,
|
||||
numberOfDataPoints: Int,
|
||||
gradientColor: ColorGradient,
|
||||
touchLocation: CGFloat) {
|
||||
self.value = value
|
||||
self.index = index
|
||||
self.width = width
|
||||
self.numberOfDataPoints = numberOfDataPoints
|
||||
self.gradientColor = gradientColor
|
||||
self.touchLocation = touchLocation
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(gradientColor.linearGradient(from: .bottom, to: .top))
|
||||
}
|
||||
.frame(width: CGFloat(self.cellWidth))
|
||||
.scaleEffect(CGSize(width: 1, height: self.firstDisplay ? 0.0 : self.value), anchor: .bottom)
|
||||
.onAppear {
|
||||
self.firstDisplay = false
|
||||
}
|
||||
.onDisappear {
|
||||
self.firstDisplay = true
|
||||
}
|
||||
.transition(.slide)
|
||||
.animation(Animation.spring().delay(self.touchLocation < 0 || !firstDisplay ? Double(self.index) * 0.04 : 0))
|
||||
}
|
||||
}
|
||||
|
||||
struct BarChartCell_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
Group {
|
||||
BarChartCell(value: 0, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
|
||||
|
||||
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
|
||||
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat())
|
||||
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat())
|
||||
}
|
||||
|
||||
Group {
|
||||
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
|
||||
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat())
|
||||
BarChartCell(value: 1, width: 50, numberOfDataPoints: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat())
|
||||
}.environment(\.colorScheme, .dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import SwiftUI
|
||||
|
||||
public struct BarChartRow: View {
|
||||
@EnvironmentObject var chartValue: ChartValue
|
||||
@ObservedObject var chartData: ChartData
|
||||
@State var touchLocation: CGFloat = -1.0
|
||||
|
||||
enum Constant {
|
||||
static let spacing: CGFloat = 16.0
|
||||
}
|
||||
|
||||
var style: ChartStyle
|
||||
|
||||
var maxValue: Double {
|
||||
guard let max = chartData.data.max() else {
|
||||
return 1
|
||||
}
|
||||
return max != 0 ? max : 1
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
GeometryReader { geometry in
|
||||
HStack(alignment: .bottom,
|
||||
spacing: (geometry.frame(in: .local).width - Constant.spacing) / CGFloat(self.chartData.data.count * 3)) {
|
||||
ForEach(0..<self.chartData.data.count, id: \.self) { index in
|
||||
BarChartCell(value: self.normalizedValue(index: index),
|
||||
index: index,
|
||||
width: Float(geometry.frame(in: .local).width - Constant.spacing),
|
||||
numberOfDataPoints: self.chartData.data.count,
|
||||
gradientColor: self.style.foregroundColor.rotate(for: index),
|
||||
touchLocation: self.touchLocation)
|
||||
.scaleEffect(self.getScaleSize(touchLocation: self.touchLocation, index: index), anchor: .bottom)
|
||||
.animation(Animation.easeIn(duration: 0.2))
|
||||
}
|
||||
// .drawingGroup()
|
||||
}
|
||||
.padding([.top, .leading, .trailing], 10)
|
||||
.gesture(DragGesture()
|
||||
.onChanged({ value in
|
||||
let width = geometry.frame(in: .local).width
|
||||
self.touchLocation = value.location.x/width
|
||||
if let currentValue = self.getCurrentValue(width: width) {
|
||||
self.chartValue.currentValue = currentValue
|
||||
self.chartValue.interactionInProgress = true
|
||||
}
|
||||
})
|
||||
.onEnded({ value in
|
||||
self.chartValue.interactionInProgress = false
|
||||
self.touchLocation = -1
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizedValue(index: Int) -> Double {
|
||||
return Double(chartData.data[index])/Double(maxValue)
|
||||
}
|
||||
|
||||
func getScaleSize(touchLocation: CGFloat, index: Int) -> CGSize {
|
||||
if touchLocation > CGFloat(index)/CGFloat(chartData.data.count) &&
|
||||
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.data[index]
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,14 @@ import SwiftUI
|
||||
|
||||
struct IndicatorPoint: View {
|
||||
var body: some View {
|
||||
ZStack{
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Colors.IndicatorKnob)
|
||||
.fill(ChartColors.indicatorKnob)
|
||||
Circle()
|
||||
.stroke(Color.white, style: StrokeStyle(lineWidth: 4))
|
||||
}
|
||||
.frame(width: 14, height: 14)
|
||||
.shadow(color: Colors.LegendColor, radius: 6, x: 0, y: 6)
|
||||
.shadow(color: ChartColors.legendColor, radius: 6, x: 0, y: 6)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import SwiftUI
|
||||
|
||||
public struct Line: View {
|
||||
@EnvironmentObject var chartValue: ChartValue
|
||||
@State var frame: CGRect = .zero
|
||||
@ObservedObject var chartData: ChartData
|
||||
|
||||
var style: ChartStyle
|
||||
|
||||
@State var showIndicator: Bool = false
|
||||
@State var touchLocation: CGPoint = .zero
|
||||
@State private var showFull: Bool = false
|
||||
@State var showBackground: Bool = true
|
||||
var curvedLines: Bool = true
|
||||
var step: CGPoint {
|
||||
return CGPoint.getStep(frame: frame, data: chartData.data)
|
||||
}
|
||||
|
||||
var path: Path {
|
||||
let points = chartData.data
|
||||
|
||||
if curvedLines {
|
||||
return Path.quadCurvedPathWithPoints(points: points,
|
||||
step: step,
|
||||
globalOffset: nil)
|
||||
}
|
||||
|
||||
return Path.linePathWithPoints(points: points, step: step)
|
||||
}
|
||||
|
||||
var closedPath: Path {
|
||||
let points = chartData.data
|
||||
|
||||
if curvedLines {
|
||||
return Path.quadClosedCurvedPathWithPoints(points: points,
|
||||
step: step,
|
||||
globalOffset: nil)
|
||||
}
|
||||
|
||||
return Path.closedLinePathWithPoints(points: points, step: step)
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
if self.showFull && self.showBackground {
|
||||
self.getBackgroundPathView()
|
||||
}
|
||||
self.getLinePathView()
|
||||
if self.showIndicator {
|
||||
IndicatorPoint()
|
||||
.position(self.getClosestPointOnPath(touchLocation: self.touchLocation))
|
||||
.rotationEffect(.degrees(180), anchor: .center)
|
||||
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
self.frame = geometry.frame(in: .local)
|
||||
}
|
||||
.gesture(DragGesture()
|
||||
.onChanged({ value in
|
||||
self.touchLocation = value.location
|
||||
self.showIndicator = true
|
||||
self.getClosestDataPoint(point: self.getClosestPointOnPath(touchLocation: value.location))
|
||||
self.chartValue.interactionInProgress = true
|
||||
})
|
||||
.onEnded({ value in
|
||||
self.touchLocation = .zero
|
||||
self.showIndicator = false
|
||||
self.chartValue.interactionInProgress = false
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private functions
|
||||
|
||||
extension Line {
|
||||
private func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint {
|
||||
let closest = self.path.point(to: touchLocation.x)
|
||||
return closest
|
||||
}
|
||||
|
||||
private func getClosestDataPoint(point: CGPoint) {
|
||||
let index = Int(round((point.x)/step.x))
|
||||
if (index >= 0 && index < self.chartData.data.count){
|
||||
self.chartValue.currentValue = self.chartData.data[index]
|
||||
}
|
||||
}
|
||||
|
||||
private func getBackgroundPathView() -> some View {
|
||||
self.closedPath
|
||||
.fill(LinearGradient(gradient: Gradient(colors: [
|
||||
style.foregroundColor.first?.startColor ?? .white,
|
||||
style.foregroundColor.first?.endColor ?? .white,
|
||||
.white]),
|
||||
startPoint: .bottom,
|
||||
endPoint: .top))
|
||||
.rotationEffect(.degrees(180), anchor: .center)
|
||||
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
|
||||
.opacity(0.2)
|
||||
.transition(.opacity)
|
||||
.animation(.easeIn(duration: 1.6))
|
||||
}
|
||||
|
||||
private func getLinePathView() -> some View {
|
||||
self.path
|
||||
.trim(from: 0, to: self.showFull ? 1:0)
|
||||
.stroke(LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient,
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing),
|
||||
style: StrokeStyle(lineWidth: 3, lineJoin: .round))
|
||||
.rotationEffect(.degrees(180), anchor: .center)
|
||||
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
|
||||
.animation(Animation.easeOut(duration: 1.2))
|
||||
.onAppear {
|
||||
self.showFull = true
|
||||
}
|
||||
.onDisappear {
|
||||
self.showFull = false
|
||||
}
|
||||
.drawingGroup()
|
||||
}
|
||||
}
|
||||
|
||||
struct Line_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
Line(chartData: ChartData([8, 23, 32, 7, 23, 43]), style: blackLineStyle)
|
||||
Line(chartData: ChartData([8, 23, 32, 7, 23, 43]), style: redLineStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let blackLineStyle = ChartStyle(backgroundColor: ColorGradient(.white), foregroundColor: ColorGradient(.black))
|
||||
private let redLineStyle = ChartStyle(backgroundColor: .whiteBlack, foregroundColor: ColorGradient(.red))
|
||||
@@ -0,0 +1,14 @@
|
||||
import SwiftUI
|
||||
|
||||
public struct LineChart: View, ChartBase {
|
||||
public var chartData = ChartData()
|
||||
|
||||
@EnvironmentObject var data: ChartData
|
||||
@EnvironmentObject var style: ChartStyle
|
||||
|
||||
public var body: some View {
|
||||
Line(chartData: data, style: style)
|
||||
}
|
||||
|
||||
public init() {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import SwiftUI
|
||||
|
||||
public struct PieChart: View, ChartBase {
|
||||
public var chartData = ChartData()
|
||||
|
||||
@EnvironmentObject var data: ChartData
|
||||
@EnvironmentObject var style: ChartStyle
|
||||
|
||||
public var body: some View {
|
||||
PieChartRow(chartData: data, style: style)
|
||||
}
|
||||
|
||||
public init() {}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// PieChartCell.swift
|
||||
// SwiftUICharts
|
||||
//
|
||||
// Created by Nicolas Savoini on 2020-05-24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PieSlice: Identifiable {
|
||||
var id = UUID()
|
||||
var startDeg: Double
|
||||
var endDeg: Double
|
||||
var value: Double
|
||||
}
|
||||
|
||||
public struct PieChartCell: View {
|
||||
@State private var show: Bool = false
|
||||
var rect: CGRect
|
||||
var radius: CGFloat {
|
||||
return min(rect.width, rect.height)/2
|
||||
}
|
||||
var startDeg: Double
|
||||
var endDeg: Double
|
||||
var path: Path {
|
||||
var path = Path()
|
||||
path.addArc(
|
||||
center: rect.mid,
|
||||
radius: self.radius,
|
||||
startAngle: Angle(degrees: self.startDeg),
|
||||
endAngle: Angle(degrees: self.endDeg),
|
||||
clockwise: false)
|
||||
path.addLine(to: rect.mid)
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
var index: Int
|
||||
|
||||
// Section line border color
|
||||
var backgroundColor: Color
|
||||
|
||||
// Section color
|
||||
var accentColor: ColorGradient
|
||||
|
||||
public var body: some View {
|
||||
Group {
|
||||
path
|
||||
.fill(self.accentColor.linearGradient(from: .bottom, to: .top))
|
||||
.overlay(path.stroke(self.backgroundColor, lineWidth: (startDeg == 0 && endDeg == 0 ? 0 : 2)))
|
||||
.scaleEffect(self.show ? 1 : 0)
|
||||
.animation(Animation.spring().delay(Double(self.index) * 0.04))
|
||||
.onAppear {
|
||||
self.show = true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PieChartCell_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
|
||||
GeometryReader { geometry in
|
||||
PieChartCell(
|
||||
rect: geometry.frame(in: .local),
|
||||
startDeg: 00.0,
|
||||
endDeg: 90.0,
|
||||
index: 0,
|
||||
backgroundColor: Color.red,
|
||||
accentColor: ColorGradient.greenRed)
|
||||
}.frame(width: 100, height: 100)
|
||||
|
||||
GeometryReader { geometry in
|
||||
PieChartCell(
|
||||
rect: geometry.frame(in: .local),
|
||||
startDeg: 0.0,
|
||||
endDeg: 90.0,
|
||||
index: 0,
|
||||
backgroundColor: Color.green,
|
||||
accentColor: ColorGradient.redBlack)
|
||||
}.frame(width: 100, height: 100)
|
||||
|
||||
GeometryReader { geometry in
|
||||
PieChartCell(
|
||||
rect: geometry.frame(in: .local),
|
||||
startDeg: 100.0,
|
||||
endDeg: 135.0,
|
||||
index: 0,
|
||||
backgroundColor: Color.black,
|
||||
accentColor: ColorGradient.whiteBlack)
|
||||
}.frame(width: 100, height: 100)
|
||||
|
||||
GeometryReader { geometry in
|
||||
PieChartCell(
|
||||
rect: geometry.frame(in: .local),
|
||||
startDeg: 185.0,
|
||||
endDeg: 290.0,
|
||||
index: 1,
|
||||
backgroundColor: Color.purple,
|
||||
accentColor: ColorGradient(.purple))
|
||||
}.frame(width: 100, height: 100)
|
||||
|
||||
GeometryReader { geometry in
|
||||
PieChartCell(
|
||||
rect: geometry.frame(in: .local),
|
||||
startDeg: 0,
|
||||
endDeg: 0,
|
||||
index: 0,
|
||||
backgroundColor: Color.purple,
|
||||
accentColor: ColorGradient(.purple))
|
||||
}.frame(width: 100, height: 100)
|
||||
|
||||
}.previewLayout(.fixed(width: 125, height: 125))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// PieChartRow.swift
|
||||
// SwiftUICharts
|
||||
//
|
||||
// Created by Nicolas Savoini on 2020-05-24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct PieChartRow: View {
|
||||
@ObservedObject var chartData: ChartData
|
||||
|
||||
var style: ChartStyle
|
||||
|
||||
var slices: [PieSlice] {
|
||||
var tempSlices: [PieSlice] = []
|
||||
var lastEndDeg: Double = 0
|
||||
let maxValue: Double = chartData.data.reduce(0, +)
|
||||
|
||||
for slice in chartData.data {
|
||||
let normalized: Double = Double(slice) / (maxValue == 0 ? 1 : maxValue)
|
||||
let startDeg = lastEndDeg
|
||||
let endDeg = lastEndDeg + (normalized * 360)
|
||||
lastEndDeg = endDeg
|
||||
tempSlices.append(PieSlice(startDeg: startDeg, endDeg: endDeg, value: slice))
|
||||
}
|
||||
|
||||
return tempSlices
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
ForEach(0..<self.slices.count) { index in
|
||||
PieChartCell(
|
||||
rect: geometry.frame(in: .local),
|
||||
startDeg: self.slices[index].startDeg,
|
||||
endDeg: self.slices[index].endDeg,
|
||||
index: index,
|
||||
backgroundColor: self.style.backgroundColor.startColor,
|
||||
accentColor: self.style.foregroundColor.rotate(for: index)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct PieChartRow_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
//Empty Array - Default Colors.OrangeStart
|
||||
PieChartRow(
|
||||
chartData: ChartData([8, 23, 32, 7, 23, 43]),
|
||||
style: defaultMultiColorChartStyle)
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
PieChartRow(
|
||||
chartData: ChartData([8, 23, 32, 7, 23, 43]),
|
||||
style: multiColorChartStyle)
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
PieChartRow(
|
||||
chartData: ChartData([8, 23, 32, 7, 23, 43]),
|
||||
style: multiColorChartStyle)
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
}.previewLayout(.fixed(width: 125, height: 125))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private let defaultMultiColorChartStyle = ChartStyle(
|
||||
backgroundColor: Color.white,
|
||||
foregroundColor: [ColorGradient]())
|
||||
|
||||
private let multiColorChartStyle = ChartStyle(
|
||||
backgroundColor: Color.purple,
|
||||
foregroundColor: [ColorGradient.greenRed, ColorGradient.whiteBlack])
|
||||
@@ -1,282 +0,0 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by András Samu on 2019. 07. 19..
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
public struct Colors {
|
||||
public static let color1:Color = Color(hexString: "#E2FAE7")
|
||||
public static let color1Accent:Color = Color(hexString: "#72BF82")
|
||||
public static let color2:Color = Color(hexString: "#EEF1FF")
|
||||
public static let color2Accent:Color = Color(hexString: "#4266E8")
|
||||
public static let color3:Color = Color(hexString: "#FCECEA")
|
||||
public static let color3Accent:Color = Color(hexString: "#E1614C")
|
||||
public static let OrangeEnd:Color = Color(hexString: "#FF782C")
|
||||
public static let OrangeStart:Color = Color(hexString: "#EC2301")
|
||||
public static let LegendText:Color = Color(hexString: "#A7A6A8")
|
||||
public static let LegendColor:Color = Color(hexString: "#E8E7EA")
|
||||
public static let LegendDarkColor:Color = Color(hexString: "#545454")
|
||||
public static let IndicatorKnob:Color = Color(hexString: "#FF57A6")
|
||||
public static let GradientUpperBlue:Color = Color(hexString: "#C2E8FF")
|
||||
public static let GradinetUpperBlue1:Color = Color(hexString: "#A8E1FF")
|
||||
public static let GradientPurple:Color = Color(hexString: "#7B75FF")
|
||||
public static let GradientNeonBlue:Color = Color(hexString: "#6FEAFF")
|
||||
public static let GradientLowerBlue:Color = Color(hexString: "#F1F9FF")
|
||||
public static let DarkPurple:Color = Color(hexString: "#1B205E")
|
||||
public static let BorderBlue:Color = Color(hexString: "#4EBCFF")
|
||||
}
|
||||
|
||||
public struct GradientColor {
|
||||
public let start: Color
|
||||
public let end: Color
|
||||
|
||||
public init(start: Color, end: Color) {
|
||||
self.start = start
|
||||
self.end = end
|
||||
}
|
||||
|
||||
public func getGradient() -> Gradient {
|
||||
return Gradient(colors: [start, end])
|
||||
}
|
||||
}
|
||||
|
||||
public struct GradientColors {
|
||||
public static let orange = GradientColor(start: Colors.OrangeStart, end: Colors.OrangeEnd)
|
||||
public static let blue = GradientColor(start: Colors.GradientPurple, end: Colors.GradientNeonBlue)
|
||||
public static let green = GradientColor(start: Color(hexString: "0BCDF7"), end: Color(hexString: "A2FEAE"))
|
||||
public static let blu = GradientColor(start: Color(hexString: "0591FF"), end: Color(hexString: "29D9FE"))
|
||||
public static let bluPurpl = GradientColor(start: Color(hexString: "4ABBFB"), end: Color(hexString: "8C00FF"))
|
||||
public static let purple = GradientColor(start: Color(hexString: "741DF4"), end: Color(hexString: "C501B0"))
|
||||
public static let prplPink = GradientColor(start: Color(hexString: "BC05AF"), end: Color(hexString: "FF1378"))
|
||||
public static let prplNeon = GradientColor(start: Color(hexString: "FE019A"), end: Color(hexString: "FE0BF4"))
|
||||
public static let orngPink = GradientColor(start: Color(hexString: "FF8E2D"), end: Color(hexString: "FF4E7A"))
|
||||
}
|
||||
|
||||
public struct Styles {
|
||||
public static let lineChartStyleOne = ChartStyle(
|
||||
backgroundColor: Color.white,
|
||||
accentColor: Colors.OrangeStart,
|
||||
secondGradientColor: Colors.OrangeEnd,
|
||||
textColor: Color.black,
|
||||
legendTextColor: Color.gray,
|
||||
dropShadowColor: Color.gray)
|
||||
|
||||
public static let barChartStyleOrangeLight = ChartStyle(
|
||||
backgroundColor: Color.white,
|
||||
accentColor: Colors.OrangeStart,
|
||||
secondGradientColor: Colors.OrangeEnd,
|
||||
textColor: Color.black,
|
||||
legendTextColor: Color.gray,
|
||||
dropShadowColor: Color.gray)
|
||||
|
||||
public static let barChartStyleOrangeDark = ChartStyle(
|
||||
backgroundColor: Color.black,
|
||||
accentColor: Colors.OrangeStart,
|
||||
secondGradientColor: Colors.OrangeEnd,
|
||||
textColor: Color.white,
|
||||
legendTextColor: Color.gray,
|
||||
dropShadowColor: Color.gray)
|
||||
|
||||
public static let barChartStyleNeonBlueLight = ChartStyle(
|
||||
backgroundColor: Color.white,
|
||||
accentColor: Colors.GradientNeonBlue,
|
||||
secondGradientColor: Colors.GradientPurple,
|
||||
textColor: Color.black,
|
||||
legendTextColor: Color.gray,
|
||||
dropShadowColor: Color.gray)
|
||||
|
||||
public static let barChartStyleNeonBlueDark = ChartStyle(
|
||||
backgroundColor: Color.black,
|
||||
accentColor: Colors.GradientNeonBlue,
|
||||
secondGradientColor: Colors.GradientPurple,
|
||||
textColor: Color.white,
|
||||
legendTextColor: Color.gray,
|
||||
dropShadowColor: Color.gray)
|
||||
|
||||
public static let barChartMidnightGreenDark = ChartStyle(
|
||||
backgroundColor: Color(hexString: "#36534D"), //3B5147, 313D34
|
||||
accentColor: Color(hexString: "#FFD603"),
|
||||
secondGradientColor: Color(hexString: "#FFCA04"),
|
||||
textColor: Color.white,
|
||||
legendTextColor: Color(hexString: "#D2E5E1"),
|
||||
dropShadowColor: Color.gray)
|
||||
|
||||
public static let barChartMidnightGreenLight = ChartStyle(
|
||||
backgroundColor: Color.white,
|
||||
accentColor: Color(hexString: "#84A094"), //84A094 , 698378
|
||||
secondGradientColor: Color(hexString: "#50675D"),
|
||||
textColor: Color.black,
|
||||
legendTextColor:Color.gray,
|
||||
dropShadowColor: Color.gray)
|
||||
|
||||
public static let pieChartStyleOne = ChartStyle(
|
||||
backgroundColor: Color.white,
|
||||
accentColor: Colors.OrangeEnd,
|
||||
secondGradientColor: Colors.OrangeStart,
|
||||
textColor: Color.black,
|
||||
legendTextColor: Color.gray,
|
||||
dropShadowColor: Color.gray)
|
||||
|
||||
public static let lineViewDarkMode = ChartStyle(
|
||||
backgroundColor: Color.black,
|
||||
accentColor: Colors.OrangeStart,
|
||||
secondGradientColor: Colors.OrangeEnd,
|
||||
textColor: Color.white,
|
||||
legendTextColor: Color.white,
|
||||
dropShadowColor: Color.gray)
|
||||
}
|
||||
|
||||
public struct ChartForm {
|
||||
#if os(watchOS)
|
||||
public static let small = CGSize(width:120, height:90)
|
||||
public static let medium = CGSize(width:120, height:160)
|
||||
public static let large = CGSize(width:180, height:90)
|
||||
public static let extraLarge = CGSize(width:180, height:90)
|
||||
public static let detail = CGSize(width:180, height:160)
|
||||
#else
|
||||
public static let small = CGSize(width:180, height:120)
|
||||
public static let medium = CGSize(width:180, height:240)
|
||||
public static let large = CGSize(width:360, height:120)
|
||||
public static let extraLarge = CGSize(width:360, height:240)
|
||||
public static let detail = CGSize(width:180, height:120)
|
||||
#endif
|
||||
}
|
||||
|
||||
public class ChartStyle {
|
||||
public var backgroundColor: Color
|
||||
public var accentColor: Color
|
||||
public var gradientColor: GradientColor
|
||||
public var textColor: Color
|
||||
public var legendTextColor: Color
|
||||
public var dropShadowColor: Color
|
||||
public weak var darkModeStyle: ChartStyle?
|
||||
|
||||
public init(backgroundColor: Color, accentColor: Color, secondGradientColor: Color, textColor: Color, legendTextColor: Color, dropShadowColor: Color){
|
||||
self.backgroundColor = backgroundColor
|
||||
self.accentColor = accentColor
|
||||
self.gradientColor = GradientColor(start: accentColor, end: secondGradientColor)
|
||||
self.textColor = textColor
|
||||
self.legendTextColor = legendTextColor
|
||||
self.dropShadowColor = dropShadowColor
|
||||
}
|
||||
|
||||
public init(backgroundColor: Color, accentColor: Color, gradientColor: GradientColor, textColor: Color, legendTextColor: Color, dropShadowColor: Color){
|
||||
self.backgroundColor = backgroundColor
|
||||
self.accentColor = accentColor
|
||||
self.gradientColor = gradientColor
|
||||
self.textColor = textColor
|
||||
self.legendTextColor = legendTextColor
|
||||
self.dropShadowColor = dropShadowColor
|
||||
}
|
||||
|
||||
public init(formSize: CGSize){
|
||||
self.backgroundColor = Color.white
|
||||
self.accentColor = Colors.OrangeStart
|
||||
self.gradientColor = GradientColors.orange
|
||||
self.legendTextColor = Color.gray
|
||||
self.textColor = Color.black
|
||||
self.dropShadowColor = Color.gray
|
||||
}
|
||||
}
|
||||
|
||||
public class ChartData: ObservableObject, Identifiable {
|
||||
@Published var points: [(String,Double)]
|
||||
var valuesGiven: Bool = false
|
||||
var ID = UUID()
|
||||
|
||||
public init<N: BinaryFloatingPoint>(points:[N]) {
|
||||
self.points = points.map{("", Double($0))}
|
||||
}
|
||||
public init<N: BinaryInteger>(values:[(String,N)]){
|
||||
self.points = values.map{($0.0, Double($0.1))}
|
||||
self.valuesGiven = true
|
||||
}
|
||||
public init<N: BinaryFloatingPoint>(values:[(String,N)]){
|
||||
self.points = values.map{($0.0, Double($0.1))}
|
||||
self.valuesGiven = true
|
||||
}
|
||||
public init<N: BinaryInteger>(numberValues:[(N,N)]){
|
||||
self.points = numberValues.map{(String($0.0), Double($0.1))}
|
||||
self.valuesGiven = true
|
||||
}
|
||||
public init<N: BinaryFloatingPoint & LosslessStringConvertible>(numberValues:[(N,N)]){
|
||||
self.points = numberValues.map{(String($0.0), Double($0.1))}
|
||||
self.valuesGiven = true
|
||||
}
|
||||
|
||||
public func onlyPoints() -> [Double] {
|
||||
return self.points.map{ $0.1 }
|
||||
}
|
||||
}
|
||||
|
||||
public class MultiLineChartData: ChartData {
|
||||
var gradient: GradientColor
|
||||
|
||||
public init<N: BinaryFloatingPoint>(points:[N], gradient: GradientColor) {
|
||||
self.gradient = gradient
|
||||
super.init(points: points)
|
||||
}
|
||||
|
||||
public init<N: BinaryFloatingPoint>(points:[N], color: Color) {
|
||||
self.gradient = GradientColor(start: color, end: color)
|
||||
super.init(points: points)
|
||||
}
|
||||
|
||||
public func getGradient() -> GradientColor {
|
||||
return self.gradient
|
||||
}
|
||||
}
|
||||
|
||||
public class TestData{
|
||||
static public var data:ChartData = ChartData(points: [37,72,51,22,39,47,66,85,50])
|
||||
static public var values:ChartData = ChartData(values: [("2017 Q3",220),
|
||||
("2017 Q4",1550),
|
||||
("2018 Q1",8180),
|
||||
("2018 Q2",18440),
|
||||
("2018 Q3",55840),
|
||||
("2018 Q4",63150), ("2019 Q1",50900), ("2019 Q2",77550), ("2019 Q3",79600), ("2019 Q4",92550)])
|
||||
|
||||
}
|
||||
|
||||
extension Color {
|
||||
init(hexString: String) {
|
||||
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var int = UInt64()
|
||||
Scanner(string: hex).scanHexInt64(&int)
|
||||
let r, g, b: UInt64
|
||||
switch hex.count {
|
||||
case 3: // RGB (12-bit)
|
||||
(r, g, b) = ((int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
||||
case 6: // RGB (24-bit)
|
||||
(r, g, b) = (int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||
case 8: // ARGB (32-bit)
|
||||
(r, g, b) = (int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
||||
default:
|
||||
(r, g, b) = (0, 0, 0)
|
||||
}
|
||||
self.init(red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255)
|
||||
}
|
||||
}
|
||||
|
||||
class HapticFeedback {
|
||||
#if os(watchOS)
|
||||
//watchOS implementation
|
||||
static func playSelection() -> Void {
|
||||
WKInterfaceDevice.current().play(.click)
|
||||
}
|
||||
#elseif os(iOS)
|
||||
//iOS implementation
|
||||
let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
|
||||
static func playSelection() -> Void {
|
||||
UISelectionFeedbackGenerator().selectionChanged()
|
||||
}
|
||||
#else
|
||||
static func playSelection() -> Void {
|
||||
//No-op
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
//
|
||||
// Legend.swift
|
||||
// LineChart
|
||||
//
|
||||
// Created by András Samu on 2019. 09. 02..
|
||||
// Copyright © 2019. András Samu. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct Legend: View {
|
||||
@ObservedObject var data: ChartData
|
||||
@Binding var frame: CGRect
|
||||
@Binding var hideHorizontalLines: Bool
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
var specifier: String = "%.2f"
|
||||
let padding:CGFloat = 3
|
||||
|
||||
var stepWidth: CGFloat {
|
||||
if data.points.count < 2 {
|
||||
return 0
|
||||
}
|
||||
return frame.size.width / CGFloat(data.points.count-1)
|
||||
}
|
||||
var stepHeight: CGFloat {
|
||||
let points = self.data.onlyPoints()
|
||||
if let min = points.min(), let max = points.max(), min != max {
|
||||
if (min < 0){
|
||||
return (frame.size.height-padding) / CGFloat(max - min)
|
||||
}else{
|
||||
return (frame.size.height-padding) / CGFloat(max - min)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var min: CGFloat {
|
||||
let points = self.data.onlyPoints()
|
||||
return CGFloat(points.min() ?? 0)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topLeading){
|
||||
ForEach((0...4), id: \.self) { height in
|
||||
HStack(alignment: .center){
|
||||
Text("\(self.getYLegendSafe(height: height), specifier: specifier)").offset(x: 0, y: self.getYposition(height: height) )
|
||||
.foregroundColor(Colors.LegendText)
|
||||
.font(.caption)
|
||||
self.line(atHeight: self.getYLegendSafe(height: height), width: self.frame.width)
|
||||
.stroke(self.colorScheme == .dark ? Colors.LegendDarkColor : Colors.LegendColor, style: StrokeStyle(lineWidth: 1.5, lineCap: .round, dash: [5,height == 0 ? 0 : 10]))
|
||||
.opacity((self.hideHorizontalLines && height != 0) ? 0 : 1)
|
||||
.rotationEffect(.degrees(180), anchor: .center)
|
||||
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
|
||||
.animation(.easeOut(duration: 0.2))
|
||||
.clipped()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func getYLegendSafe(height:Int)->CGFloat{
|
||||
if let legend = getYLegend() {
|
||||
return CGFloat(legend[height])
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func getYposition(height: Int)-> CGFloat {
|
||||
if let legend = getYLegend() {
|
||||
return (self.frame.height-((CGFloat(legend[height]) - min)*self.stepHeight))-(self.frame.height/2)
|
||||
}
|
||||
return 0
|
||||
|
||||
}
|
||||
|
||||
func line(atHeight: CGFloat, width: CGFloat) -> Path {
|
||||
var hLine = Path()
|
||||
hLine.move(to: CGPoint(x:5, y: (atHeight-min)*stepHeight))
|
||||
hLine.addLine(to: CGPoint(x: width, y: (atHeight-min)*stepHeight))
|
||||
return hLine
|
||||
}
|
||||
|
||||
func getYLegend() -> [Double]? {
|
||||
let points = self.data.onlyPoints()
|
||||
guard let max = points.max() else { return nil }
|
||||
guard let min = points.min() else { return nil }
|
||||
let step = Double(max - min)/4
|
||||
return [min+step * 0, min+step * 1, min+step * 2, min+step * 3, min+step * 4]
|
||||
}
|
||||
}
|
||||
|
||||
struct Legend_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GeometryReader{ geometry in
|
||||
Legend(data: ChartData(points: [0.2,0.4,1.4,4.5]), frame: .constant(geometry.frame(in: .local)), hideHorizontalLines: .constant(false))
|
||||
}.frame(width: 320, height: 200)
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
//
|
||||
// Line.swift
|
||||
// LineChart
|
||||
//
|
||||
// Created by András Samu on 2019. 08. 30..
|
||||
// Copyright © 2019. András Samu. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct Line: View {
|
||||
@ObservedObject var data: ChartData
|
||||
@Binding var frame: CGRect
|
||||
@Binding var touchLocation: CGPoint
|
||||
@Binding var showIndicator: Bool
|
||||
@Binding var minDataValue: Double?
|
||||
@Binding var maxDataValue: Double?
|
||||
@State private var showFull: Bool = false
|
||||
@State var showBackground: Bool = true
|
||||
var gradient: GradientColor = GradientColor(start: Colors.GradientPurple, end: Colors.GradientNeonBlue)
|
||||
var index:Int = 0
|
||||
let padding:CGFloat = 30
|
||||
var curvedLines: Bool = true
|
||||
var stepWidth: CGFloat {
|
||||
if data.points.count < 2 {
|
||||
return 0
|
||||
}
|
||||
return frame.size.width / CGFloat(data.points.count-1)
|
||||
}
|
||||
var stepHeight: CGFloat {
|
||||
var min: Double?
|
||||
var max: Double?
|
||||
let points = self.data.onlyPoints()
|
||||
if minDataValue != nil && maxDataValue != nil {
|
||||
min = minDataValue!
|
||||
max = maxDataValue!
|
||||
}else if let minPoint = points.min(), let maxPoint = points.max(), minPoint != maxPoint {
|
||||
min = minPoint
|
||||
max = maxPoint
|
||||
}else {
|
||||
return 0
|
||||
}
|
||||
if let min = min, let max = max, min != max {
|
||||
if (min <= 0){
|
||||
return (frame.size.height-padding) / CGFloat(max - min)
|
||||
}else{
|
||||
return (frame.size.height-padding) / CGFloat(max - min)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
var path: Path {
|
||||
let points = self.data.onlyPoints()
|
||||
return curvedLines ? Path.quadCurvedPathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight), globalOffset: minDataValue) : Path.linePathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight))
|
||||
}
|
||||
var closedPath: Path {
|
||||
let points = self.data.onlyPoints()
|
||||
return curvedLines ? Path.quadClosedCurvedPathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight), globalOffset: minDataValue) : Path.closedLinePathWithPoints(points: points, step: CGPoint(x: stepWidth, y: stepHeight))
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
if(self.showFull && self.showBackground){
|
||||
self.closedPath
|
||||
.fill(LinearGradient(gradient: Gradient(colors: [Colors.GradientUpperBlue, .white]), startPoint: .bottom, endPoint: .top))
|
||||
.rotationEffect(.degrees(180), anchor: .center)
|
||||
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
|
||||
.transition(.opacity)
|
||||
.animation(.easeIn(duration: 1.6))
|
||||
}
|
||||
self.path
|
||||
.trim(from: 0, to: self.showFull ? 1:0)
|
||||
.stroke(LinearGradient(gradient: gradient.getGradient(), startPoint: .leading, endPoint: .trailing) ,style: StrokeStyle(lineWidth: 3, lineJoin: .round))
|
||||
.rotationEffect(.degrees(180), anchor: .center)
|
||||
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
|
||||
.animation(Animation.easeOut(duration: 1.2).delay(Double(self.index)*0.4))
|
||||
.onAppear {
|
||||
self.showFull = true
|
||||
}
|
||||
.onDisappear {
|
||||
self.showFull = false
|
||||
}
|
||||
if(self.showIndicator) {
|
||||
IndicatorPoint()
|
||||
.position(self.getClosestPointOnPath(touchLocation: self.touchLocation))
|
||||
.rotationEffect(.degrees(180), anchor: .center)
|
||||
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint {
|
||||
let closest = self.path.point(to: touchLocation.x)
|
||||
return closest
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct Line_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GeometryReader{ geometry in
|
||||
Line(data: ChartData(points: [12,-230,10,54]), frame: .constant(geometry.frame(in: .local)), touchLocation: .constant(CGPoint(x: 100, y: 12)), showIndicator: .constant(true), minDataValue: .constant(nil), maxDataValue: .constant(nil))
|
||||
}.frame(width: 320, height: 160)
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
//
|
||||
// LineCard.swift
|
||||
// LineChart
|
||||
//
|
||||
// Created by András Samu on 2019. 08. 31..
|
||||
// Copyright © 2019. András Samu. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct LineChartView: View {
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
@ObservedObject var data:ChartData
|
||||
public var title: String
|
||||
public var legend: String?
|
||||
public var style: ChartStyle
|
||||
public var darkModeStyle: ChartStyle
|
||||
|
||||
public var formSize:CGSize
|
||||
public var dropShadow: Bool
|
||||
public var valueSpecifier:String
|
||||
|
||||
@State private var touchLocation:CGPoint = .zero
|
||||
@State private var showIndicatorDot: Bool = false
|
||||
@State private var currentValue: Double = 2 {
|
||||
didSet{
|
||||
if (oldValue != self.currentValue && showIndicatorDot) {
|
||||
HapticFeedback.playSelection()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
var frame = CGSize(width: 180, height: 120)
|
||||
private var rateValue: Int?
|
||||
|
||||
public init(data: [Double],
|
||||
title: String,
|
||||
legend: String? = nil,
|
||||
style: ChartStyle = Styles.lineChartStyleOne,
|
||||
form: CGSize? = ChartForm.medium,
|
||||
rateValue: Int?,
|
||||
dropShadow: Bool? = true,
|
||||
valueSpecifier: String? = "%.1f") {
|
||||
|
||||
self.data = ChartData(points: data)
|
||||
self.title = title
|
||||
self.legend = legend
|
||||
self.style = style
|
||||
self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.lineViewDarkMode
|
||||
self.formSize = form!
|
||||
frame = CGSize(width: self.formSize.width, height: self.formSize.height/2)
|
||||
self.dropShadow = dropShadow!
|
||||
self.valueSpecifier = valueSpecifier!
|
||||
self.rateValue = rateValue
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack(alignment: .center){
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor)
|
||||
.frame(width: frame.width, height: 240, alignment: .center)
|
||||
.shadow(color: self.style.dropShadowColor, radius: self.dropShadow ? 8 : 0)
|
||||
VStack(alignment: .leading){
|
||||
if(!self.showIndicatorDot){
|
||||
VStack(alignment: .leading, spacing: 8){
|
||||
Text(self.title)
|
||||
.font(.title)
|
||||
.bold()
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
|
||||
if (self.legend != nil){
|
||||
Text(self.legend!)
|
||||
.font(.callout)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor :self.style.legendTextColor)
|
||||
}
|
||||
HStack {
|
||||
|
||||
if let rateValue = self.rateValue
|
||||
{
|
||||
if (rateValue ?? 0 >= 0){
|
||||
Image(systemName: "arrow.up")
|
||||
}else{
|
||||
Image(systemName: "arrow.down")
|
||||
}
|
||||
Text("\(rateValue!)%")
|
||||
}
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
.animation(.easeIn(duration: 0.1))
|
||||
.padding([.leading, .top])
|
||||
}else{
|
||||
HStack{
|
||||
Spacer()
|
||||
Text("\(self.currentValue, specifier: self.valueSpecifier)")
|
||||
.font(.system(size: 41, weight: .bold, design: .default))
|
||||
.offset(x: 0, y: 30)
|
||||
Spacer()
|
||||
}
|
||||
.transition(.scale)
|
||||
}
|
||||
Spacer()
|
||||
GeometryReader{ geometry in
|
||||
Line(data: self.data,
|
||||
frame: .constant(geometry.frame(in: .local)),
|
||||
touchLocation: self.$touchLocation,
|
||||
showIndicator: self.$showIndicatorDot,
|
||||
minDataValue: .constant(nil),
|
||||
maxDataValue: .constant(nil)
|
||||
)
|
||||
}
|
||||
.frame(width: frame.width, height: frame.height)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
.offset(x: 0, y: 0)
|
||||
}.frame(width: self.formSize.width, height: self.formSize.height)
|
||||
}
|
||||
.gesture(DragGesture()
|
||||
.onChanged({ value in
|
||||
self.touchLocation = value.location
|
||||
self.showIndicatorDot = true
|
||||
self.getClosestDataPoint(toPoint: value.location, width:self.frame.width, height: self.frame.height)
|
||||
})
|
||||
.onEnded({ value in
|
||||
self.showIndicatorDot = false
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@discardableResult func getClosestDataPoint(toPoint: CGPoint, width:CGFloat, height: CGFloat) -> CGPoint {
|
||||
let points = self.data.onlyPoints()
|
||||
let stepWidth: CGFloat = width / CGFloat(points.count-1)
|
||||
let stepHeight: CGFloat = height / CGFloat(points.max()! + points.min()!)
|
||||
|
||||
let index:Int = Int(round((toPoint.x)/stepWidth))
|
||||
if (index >= 0 && index < points.count){
|
||||
self.currentValue = points[index]
|
||||
return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(points[index])*stepHeight)
|
||||
}
|
||||
return .zero
|
||||
}
|
||||
}
|
||||
|
||||
struct WidgetView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
LineChartView(data: [8,23,54,32,12,37,7,23,43], title: "Line chart", legend: "Basic")
|
||||
.environment(\.colorScheme, .light)
|
||||
|
||||
LineChartView(data: [282.502, 284.495, 283.51, 285.019, 285.197, 286.118, 288.737, 288.455, 289.391, 287.691, 285.878, 286.46, 286.252, 284.652, 284.129, 284.188], title: "Line chart", legend: "Basic")
|
||||
.environment(\.colorScheme, .light)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
//
|
||||
// LineView.swift
|
||||
// LineChart
|
||||
//
|
||||
// Created by András Samu on 2019. 09. 02..
|
||||
// Copyright © 2019. András Samu. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct LineView: View {
|
||||
@ObservedObject var data: ChartData
|
||||
public var title: String?
|
||||
public var legend: String?
|
||||
public var style: ChartStyle
|
||||
public var darkModeStyle: ChartStyle
|
||||
public var valueSpecifier: String
|
||||
public var legendSpecifier: String
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
@State private var showLegend = false
|
||||
@State private var dragLocation:CGPoint = .zero
|
||||
@State private var indicatorLocation:CGPoint = .zero
|
||||
@State private var closestPoint: CGPoint = .zero
|
||||
@State private var opacity:Double = 0
|
||||
@State private var currentDataNumber: Double = 0
|
||||
@State private var hideHorizontalLines: Bool = false
|
||||
|
||||
public init(data: [Double],
|
||||
title: String? = nil,
|
||||
legend: String? = nil,
|
||||
style: ChartStyle = Styles.lineChartStyleOne,
|
||||
valueSpecifier: String? = "%.1f",
|
||||
legendSpecifier: String? = "%.2f") {
|
||||
|
||||
self.data = ChartData(points: data)
|
||||
self.title = title
|
||||
self.legend = legend
|
||||
self.style = style
|
||||
self.valueSpecifier = valueSpecifier!
|
||||
self.legendSpecifier = legendSpecifier!
|
||||
self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.lineViewDarkMode
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
GeometryReader{ geometry in
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Group{
|
||||
if (self.title != nil){
|
||||
Text(self.title!)
|
||||
.font(.title)
|
||||
.bold().foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
|
||||
}
|
||||
if (self.legend != nil){
|
||||
Text(self.legend!)
|
||||
.font(.callout)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor)
|
||||
}
|
||||
}.offset(x: 0, y: 20)
|
||||
ZStack{
|
||||
GeometryReader{ reader in
|
||||
Rectangle()
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor)
|
||||
if(self.showLegend){
|
||||
Legend(data: self.data,
|
||||
frame: .constant(reader.frame(in: .local)), hideHorizontalLines: self.$hideHorizontalLines, specifier: legendSpecifier)
|
||||
.transition(.opacity)
|
||||
.animation(Animation.easeOut(duration: 1).delay(1))
|
||||
}
|
||||
Line(data: self.data,
|
||||
frame: .constant(CGRect(x: 0, y: 0, width: reader.frame(in: .local).width - 30, height: reader.frame(in: .local).height + 25)),
|
||||
touchLocation: self.$indicatorLocation,
|
||||
showIndicator: self.$hideHorizontalLines,
|
||||
minDataValue: .constant(nil),
|
||||
maxDataValue: .constant(nil),
|
||||
showBackground: false,
|
||||
gradient: self.style.gradientColor
|
||||
)
|
||||
.offset(x: 30, y: 0)
|
||||
.onAppear(){
|
||||
self.showLegend = true
|
||||
}
|
||||
.onDisappear(){
|
||||
self.showLegend = false
|
||||
}
|
||||
}
|
||||
.frame(width: geometry.frame(in: .local).size.width, height: 240)
|
||||
.offset(x: 0, y: 40 )
|
||||
MagnifierRect(currentNumber: self.$currentDataNumber, valueSpecifier: self.valueSpecifier)
|
||||
.opacity(self.opacity)
|
||||
.offset(x: self.dragLocation.x - geometry.frame(in: .local).size.width/2, y: 36)
|
||||
}
|
||||
.frame(width: geometry.frame(in: .local).size.width, height: 240)
|
||||
.gesture(DragGesture()
|
||||
.onChanged({ value in
|
||||
self.dragLocation = value.location
|
||||
self.indicatorLocation = CGPoint(x: max(value.location.x-30,0), y: 32)
|
||||
self.opacity = 1
|
||||
self.closestPoint = self.getClosestDataPoint(toPoint: value.location, width: geometry.frame(in: .local).size.width-30, height: 240)
|
||||
self.hideHorizontalLines = true
|
||||
})
|
||||
.onEnded({ value in
|
||||
self.opacity = 0
|
||||
self.hideHorizontalLines = false
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getClosestDataPoint(toPoint: CGPoint, width:CGFloat, height: CGFloat) -> CGPoint {
|
||||
let points = self.data.onlyPoints()
|
||||
let stepWidth: CGFloat = width / CGFloat(points.count-1)
|
||||
let stepHeight: CGFloat = height / CGFloat(points.max()! + points.min()!)
|
||||
|
||||
let index:Int = Int(floor((toPoint.x-15)/stepWidth))
|
||||
if (index >= 0 && index < points.count){
|
||||
self.currentDataNumber = points[index]
|
||||
return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(points[index])*stepHeight)
|
||||
}
|
||||
return .zero
|
||||
}
|
||||
}
|
||||
|
||||
struct LineView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
LineView(data: [8,23,54,32,12,37,7,23,43], title: "Full chart", style: Styles.lineChartStyleOne)
|
||||
|
||||
LineView(data: [282.502, 284.495, 283.51, 285.019, 285.197, 286.118, 288.737, 288.455, 289.391, 287.691, 285.878, 286.46, 286.252, 284.652, 284.129, 284.188], title: "Full chart", style: Styles.lineChartStyleOne)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
//
|
||||
// MagnifierRect.swift
|
||||
//
|
||||
//
|
||||
// Created by Samu András on 2020. 03. 04..
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct MagnifierRect: View {
|
||||
@Binding var currentNumber: Double
|
||||
var valueSpecifier:String
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
public var body: some View {
|
||||
ZStack{
|
||||
Text("\(self.currentNumber, specifier: valueSpecifier)")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.offset(x: 0, y:-110)
|
||||
.foregroundColor(self.colorScheme == .dark ? Color.white : Color.black)
|
||||
if (self.colorScheme == .dark ){
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.stroke(Color.white, lineWidth: self.colorScheme == .dark ? 2 : 0)
|
||||
.frame(width: 60, height: 260)
|
||||
}else{
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.frame(width: 60, height: 280)
|
||||
.foregroundColor(Color.white)
|
||||
.shadow(color: Colors.LegendText, radius: 12, x: 0, y: 6 )
|
||||
.blendMode(.multiply)
|
||||
}
|
||||
}
|
||||
.offset(x: 0, y: -15)
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Samu András on 2020. 02. 19..
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct MultiLineChartView: View {
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
var data:[MultiLineChartData]
|
||||
public var title: String
|
||||
public var legend: String?
|
||||
public var style: ChartStyle
|
||||
public var darkModeStyle: ChartStyle
|
||||
public var formSize: CGSize
|
||||
public var dropShadow: Bool
|
||||
public var valueSpecifier:String
|
||||
|
||||
@State private var touchLocation:CGPoint = .zero
|
||||
@State private var showIndicatorDot: Bool = false
|
||||
@State private var currentValue: Double = 2 {
|
||||
didSet{
|
||||
if (oldValue != self.currentValue && showIndicatorDot) {
|
||||
HapticFeedback.playSelection()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var globalMin:Double {
|
||||
if let min = data.flatMap({$0.onlyPoints()}).min() {
|
||||
return min
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var globalMax:Double {
|
||||
if let max = data.flatMap({$0.onlyPoints()}).max() {
|
||||
return max
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var frame = CGSize(width: 180, height: 120)
|
||||
private var rateValue: Int?
|
||||
|
||||
public init(data: [([Double], GradientColor)],
|
||||
title: String,
|
||||
legend: String? = nil,
|
||||
style: ChartStyle = Styles.lineChartStyleOne,
|
||||
form: CGSize = ChartForm.medium,
|
||||
rateValue: Int? = nil,
|
||||
dropShadow: Bool = true,
|
||||
valueSpecifier: String = "%.1f") {
|
||||
|
||||
self.data = data.map({ MultiLineChartData(points: $0.0, gradient: $0.1)})
|
||||
self.title = title
|
||||
self.legend = legend
|
||||
self.style = style
|
||||
self.darkModeStyle = style.darkModeStyle != nil ? style.darkModeStyle! : Styles.lineViewDarkMode
|
||||
self.formSize = form
|
||||
frame = CGSize(width: self.formSize.width, height: self.formSize.height/2)
|
||||
self.rateValue = rateValue
|
||||
self.dropShadow = dropShadow
|
||||
self.valueSpecifier = valueSpecifier
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack(alignment: .center){
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(self.colorScheme == .dark ? self.darkModeStyle.backgroundColor : self.style.backgroundColor)
|
||||
.frame(width: frame.width, height: 240, alignment: .center)
|
||||
.shadow(radius: self.dropShadow ? 8 : 0)
|
||||
VStack(alignment: .leading){
|
||||
if(!self.showIndicatorDot){
|
||||
VStack(alignment: .leading, spacing: 8){
|
||||
Text(self.title)
|
||||
.font(.title)
|
||||
.bold()
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.textColor : self.style.textColor)
|
||||
if (self.legend != nil){
|
||||
Text(self.legend!)
|
||||
.font(.callout)
|
||||
.foregroundColor(self.colorScheme == .dark ? self.darkModeStyle.legendTextColor : self.style.legendTextColor)
|
||||
}
|
||||
HStack {
|
||||
if (rateValue ?? 0 >= 0){
|
||||
Image(systemName: "arrow.up")
|
||||
}else{
|
||||
Image(systemName: "arrow.down")
|
||||
}
|
||||
Text("\(rateValue ?? 0)%")
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
.animation(.easeIn(duration: 0.1))
|
||||
.padding([.leading, .top])
|
||||
}else{
|
||||
HStack{
|
||||
Spacer()
|
||||
Text("\(self.currentValue, specifier: self.valueSpecifier)")
|
||||
.font(.system(size: 41, weight: .bold, design: .default))
|
||||
.offset(x: 0, y: 30)
|
||||
Spacer()
|
||||
}
|
||||
.transition(.scale)
|
||||
}
|
||||
Spacer()
|
||||
GeometryReader{ geometry in
|
||||
ZStack{
|
||||
ForEach(0..<self.data.count, id: \.self) { i in
|
||||
Line(data: self.data[i],
|
||||
frame: .constant(geometry.frame(in: .local)),
|
||||
touchLocation: self.$touchLocation,
|
||||
showIndicator: self.$showIndicatorDot,
|
||||
minDataValue: .constant(self.globalMin),
|
||||
maxDataValue: .constant(self.globalMax),
|
||||
showBackground: false,
|
||||
gradient: self.data[i].getGradient(),
|
||||
index: i)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: frame.width, height: frame.height + 30)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
.offset(x: 0, y: 0)
|
||||
}.frame(width: self.formSize.width, height: self.formSize.height)
|
||||
}
|
||||
.gesture(DragGesture()
|
||||
.onChanged({ value in
|
||||
// self.touchLocation = value.location
|
||||
// self.showIndicatorDot = true
|
||||
// self.getClosestDataPoint(toPoint: value.location, width:self.frame.width, height: self.frame.height)
|
||||
})
|
||||
.onEnded({ value in
|
||||
self.showIndicatorDot = false
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// @discardableResult func getClosestDataPoint(toPoint: CGPoint, width:CGFloat, height: CGFloat) -> CGPoint {
|
||||
// let points = self.data.onlyPoints()
|
||||
// let stepWidth: CGFloat = width / CGFloat(points.count-1)
|
||||
// let stepHeight: CGFloat = height / CGFloat(points.max()! + points.min()!)
|
||||
//
|
||||
// let index:Int = Int(round((toPoint.x)/stepWidth))
|
||||
// if (index >= 0 && index < points.count){
|
||||
// self.currentValue = points[index]
|
||||
// return CGPoint(x: CGFloat(index)*stepWidth, y: CGFloat(points[index])*stepHeight)
|
||||
// }
|
||||
// return .zero
|
||||
// }
|
||||
}
|
||||
|
||||
struct MultiWidgetView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
MultiLineChartView(data: [([8,23,54,32,12,37,7,23,43], GradientColors.orange)], title: "Line chart", legend: "Basic")
|
||||
.environment(\.colorScheme, .light)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
//
|
||||
// PieChartCell.swift
|
||||
// ChartView
|
||||
//
|
||||
// Created by András Samu on 2019. 06. 12..
|
||||
// Copyright © 2019. András Samu. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PieSlice: Identifiable {
|
||||
var id = UUID()
|
||||
var startDeg: Double
|
||||
var endDeg: Double
|
||||
var value: Double
|
||||
var normalizedValue: Double
|
||||
}
|
||||
|
||||
public struct PieChartCell : View {
|
||||
@State private var show:Bool = false
|
||||
var rect: CGRect
|
||||
var radius: CGFloat {
|
||||
return min(rect.width, rect.height)/2
|
||||
}
|
||||
var startDeg: Double
|
||||
var endDeg: Double
|
||||
var path: Path {
|
||||
var path = Path()
|
||||
path.addArc(center:rect.mid , radius:self.radius, startAngle: Angle(degrees: self.startDeg), endAngle: Angle(degrees: self.endDeg), clockwise: false)
|
||||
path.addLine(to: rect.mid)
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
var index: Int
|
||||
var backgroundColor:Color
|
||||
var accentColor:Color
|
||||
public var body: some View {
|
||||
path
|
||||
.fill()
|
||||
.foregroundColor(self.accentColor)
|
||||
.overlay(path.stroke(self.backgroundColor, lineWidth: 2))
|
||||
.scaleEffect(self.show ? 1 : 0)
|
||||
.animation(Animation.spring().delay(Double(self.index) * 0.04))
|
||||
.onAppear(){
|
||||
self.show = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CGRect {
|
||||
var mid: CGPoint {
|
||||
return CGPoint(x:self.midX, y: self.midY)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct PieChartCell_Previews : PreviewProvider {
|
||||
static var previews: some View {
|
||||
GeometryReader { geometry in
|
||||
PieChartCell(rect: geometry.frame(in: .local),startDeg: 0.0,endDeg: 90.0, index: 0, backgroundColor: Color(red: 252.0/255.0, green: 236.0/255.0, blue: 234.0/255.0), accentColor: Color(red: 225.0/255.0, green: 97.0/255.0, blue: 76.0/255.0))
|
||||
}.frame(width:100, height:100)
|
||||
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by 曾文志 on 2020/7/30.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
func isPointInCircle(point: CGPoint, circleRect: CGRect) -> Bool {
|
||||
let r = min(circleRect.width, circleRect.height) / 2
|
||||
let center = CGPoint(x: circleRect.midX, y: circleRect.midY)
|
||||
let dx = point.x - center.x
|
||||
let dy = point.y - center.y
|
||||
let distance = sqrt(dx * dx + dy * dy)
|
||||
return distance <= r
|
||||
}
|
||||
|
||||
func degree(for point: CGPoint, inCircleRect circleRect: CGRect) -> Double {
|
||||
let center = CGPoint(x: circleRect.midX, y: circleRect.midY)
|
||||
let dx = point.x - center.x
|
||||
let dy = point.y - center.y
|
||||
let acuteDegree = Double(atan(dy / dx)) * (180 / .pi)
|
||||
|
||||
let isInBottomRight = dx >= 0 && dy >= 0
|
||||
let isInBottomLeft = dx <= 0 && dy >= 0
|
||||
let isInTopLeft = dx <= 0 && dy <= 0
|
||||
let isInTopRight = dx >= 0 && dy <= 0
|
||||
|
||||
if isInBottomRight {
|
||||
return acuteDegree
|
||||
} else if isInBottomLeft {
|
||||
return 180 - abs(acuteDegree)
|
||||
} else if isInTopLeft {
|
||||
return 180 + abs(acuteDegree)
|
||||
} else if isInTopRight {
|
||||
return 360 - abs(acuteDegree)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -1,79 +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: [Double]
|
||||
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
|
||||
}
|
||||
|
||||
@Binding var showValue: Bool
|
||||
@Binding var currentValue: Double
|
||||
|
||||
@State private var currentTouchedIndex = -1 {
|
||||
didSet {
|
||||
if oldValue != currentTouchedIndex {
|
||||
showValue = currentTouchedIndex != -1
|
||||
currentValue = showValue ? slices[currentTouchedIndex].value : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.scaleEffect(self.currentTouchedIndex == i ? 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)
|
||||
self.currentTouchedIndex = self.slices.firstIndex(where: { $0.startDeg < touchDegree && $0.endDeg > touchDegree }) ?? -1
|
||||
} else {
|
||||
self.currentTouchedIndex = -1
|
||||
}
|
||||
})
|
||||
.onEnded({ value in
|
||||
self.currentTouchedIndex = -1
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct PieChartRow_Previews : PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
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), showValue: Binding.constant(false), currentValue: Binding.constant(0))
|
||||
.frame(width: 100, height: 100)
|
||||
PieChartRow(data:[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), showValue: Binding.constant(false), currentValue: Binding.constant(0))
|
||||
.frame(width: 100, height: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,84 +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: [Double]
|
||||
public var title: String
|
||||
public var legend: String?
|
||||
public var style: ChartStyle
|
||||
public var formSize:CGSize
|
||||
public var dropShadow: Bool
|
||||
public var valueSpecifier:String
|
||||
|
||||
@State private var showValue = false
|
||||
@State private var currentValue: Double = 0 {
|
||||
didSet{
|
||||
if(oldValue != self.currentValue && self.showValue) {
|
||||
HapticFeedback.playSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init(data: [Double], title: String, legend: String? = nil, style: ChartStyle = Styles.pieChartStyleOne, form: CGSize? = ChartForm.medium, dropShadow: Bool? = true, valueSpecifier: String? = "%.1f"){
|
||||
self.data = data
|
||||
self.title = title
|
||||
self.legend = legend
|
||||
self.style = style
|
||||
self.formSize = form!
|
||||
if self.formSize == ChartForm.large {
|
||||
self.formSize = ChartForm.extraLarge
|
||||
}
|
||||
self.dropShadow = dropShadow!
|
||||
self.valueSpecifier = valueSpecifier!
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack{
|
||||
Rectangle()
|
||||
.fill(self.style.backgroundColor)
|
||||
.cornerRadius(20)
|
||||
.shadow(color: self.style.dropShadowColor, radius: self.dropShadow ? 12 : 0)
|
||||
VStack(alignment: .leading){
|
||||
HStack{
|
||||
if(!showValue){
|
||||
Text(self.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(self.style.textColor)
|
||||
}else{
|
||||
Text("\(self.currentValue, specifier: self.valueSpecifier)")
|
||||
.font(.headline)
|
||||
.foregroundColor(self.style.textColor)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "chart.pie.fill")
|
||||
.imageScale(.large)
|
||||
.foregroundColor(self.style.legendTextColor)
|
||||
}.padding()
|
||||
PieChartRow(data: data, backgroundColor: self.style.backgroundColor, accentColor: self.style.accentColor, showValue: $showValue, currentValue: $currentValue)
|
||||
.foregroundColor(self.style.accentColor).padding(self.legend != nil ? 0 : 12).offset(y:self.legend != nil ? 0 : -10)
|
||||
if(self.legend != nil) {
|
||||
Text(self.legend!)
|
||||
.font(.headline)
|
||||
.foregroundColor(self.style.legendTextColor)
|
||||
.padding()
|
||||
}
|
||||
|
||||
}
|
||||
}.frame(width: self.formSize.width, height: self.formSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct PieChartView_Previews : PreviewProvider {
|
||||
static var previews: some View {
|
||||
PieChartView(data:[56,78,53,65,54], title: "Title", legend: "Legend")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Nicolas Savoini on 2020-05-25.
|
||||
//
|
||||
|
||||
@testable import SwiftUICharts
|
||||
import XCTest
|
||||
|
||||
class ArrayExtensionTests: XCTestCase {
|
||||
|
||||
func testArrayRotatingIndexEmpty() {
|
||||
let colors = [ColorGradient]()
|
||||
XCTAssertEqual(colors.rotate(for: 0), ColorGradient.orangeBright)
|
||||
}
|
||||
|
||||
func testArrayRotatingIndexOneValue() {
|
||||
let colors = [ColorGradient.greenRed]
|
||||
|
||||
XCTAssertEqual(colors.rotate(for: 0), ColorGradient.greenRed)
|
||||
XCTAssertEqual(colors.rotate(for: 1), ColorGradient.greenRed)
|
||||
XCTAssertEqual(colors.rotate(for: 2), ColorGradient.greenRed)
|
||||
}
|
||||
|
||||
func testArrayRotatingIndexLessValues() {
|
||||
let colors = [ColorGradient.greenRed, ColorGradient.whiteBlack]
|
||||
|
||||
XCTAssertEqual(colors.rotate(for: 0), ColorGradient.greenRed)
|
||||
XCTAssertEqual(colors.rotate(for: 1), ColorGradient.whiteBlack)
|
||||
XCTAssertEqual(colors.rotate(for: 2), ColorGradient.greenRed)
|
||||
XCTAssertEqual(colors.rotate(for: 3), ColorGradient.whiteBlack)
|
||||
XCTAssertEqual(colors.rotate(for: 4), ColorGradient.greenRed)
|
||||
}
|
||||
|
||||
func testArrayRotatingIndexMoreValues() {
|
||||
let colors = [ColorGradient.greenRed, ColorGradient.whiteBlack, ColorGradient.orangeBright]
|
||||
|
||||
XCTAssertEqual(colors.rotate(for: 0), ColorGradient.greenRed)
|
||||
XCTAssertEqual(colors.rotate(for: 1), ColorGradient.whiteBlack)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// CGPointExtensionTests.swift
|
||||
// SwiftUIChartsTests
|
||||
//
|
||||
// Created by Adrian Bolinger on 5/24/20.
|
||||
//
|
||||
|
||||
@testable import SwiftUICharts
|
||||
import XCTest
|
||||
|
||||
class CGPointExtensionTests: XCTestCase {
|
||||
static let twentyElementArray: [Double] = Array(repeating: Double.random(in: 1...100), count: 20)
|
||||
|
||||
func testGetStepWithOneElementArray() {
|
||||
let frame = CGRect(x: 0, y: 0, width: 300, height: 300)
|
||||
let oneElementArray: [Double] = [0.0]
|
||||
|
||||
XCTAssertEqual(CGPoint.getStep(frame: frame, data: oneElementArray), .zero)
|
||||
}
|
||||
|
||||
func testGetStepWithMultiElementArrayWithNegativeValues() {
|
||||
let frame = CGRect(x: 0, y: 0, width: 300, height: 300)
|
||||
let multiElementArray: [Double] = [-5.0, 0.0, 5.0]
|
||||
XCTAssertEqual(CGPoint.getStep(frame: frame, data: multiElementArray), CGPoint(x: 150.0, y: 27.0))
|
||||
}
|
||||
|
||||
func testGetStepWithMultiElementArrayWithPositiveValues() {
|
||||
let frame = CGRect(x: 0, y: 0, width: 300, height: 300)
|
||||
let multiElementArray: [Double] = [5.0, 10.0, 15.0]
|
||||
XCTAssertEqual(CGPoint.getStep(frame: frame, data: multiElementArray), CGPoint(x: 150.0, y: 13.5))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// ColorExtensionTests.swift
|
||||
// SwiftUIChartsTests
|
||||
//
|
||||
// Created by Adrian Bolinger on 5/24/20.
|
||||
//
|
||||
|
||||
@testable import SwiftUICharts
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
class ColorExtensionTests: XCTestCase {
|
||||
func testTwentyFourBitRGBColors() {
|
||||
let actualWhite = Color(hexString: "FFFFFF")
|
||||
let expectedWhite = Color(red: 1, green: 1, blue: 1)
|
||||
XCTAssertEqual(actualWhite, expectedWhite)
|
||||
|
||||
let actualBlack = Color(hexString: "000000")
|
||||
let expectedBlack = Color(red: 0, green: 0, blue: 0)
|
||||
XCTAssertEqual(actualBlack, expectedBlack)
|
||||
|
||||
let actualRed = Color(hexString: "FF0000")
|
||||
let expectedRed = Color(red: 255/255, green: 0, blue: 0)
|
||||
XCTAssertEqual(actualRed, expectedRed)
|
||||
|
||||
let actualGreen = Color(hexString: "00FF00")
|
||||
let expectedGreen = Color(red: 0, green: 1, blue: 0)
|
||||
XCTAssertEqual(actualGreen, expectedGreen)
|
||||
|
||||
let actualBlue = Color(hexString: "0000FF")
|
||||
let expectedBlue = Color(red: 0, green: 0, blue: 1)
|
||||
XCTAssertEqual(actualBlue, expectedBlue)
|
||||
}
|
||||
|
||||
func testTwelveBitRGBColors() {
|
||||
let actualWhite = Color(hexString: "FFF")
|
||||
let expectedWhite = Color(red: 1, green: 1, blue: 1)
|
||||
XCTAssertEqual(actualWhite, expectedWhite)
|
||||
|
||||
let actualBlack = Color(hexString: "000")
|
||||
let expectedBlack = Color(red: 0, green: 0, blue: 0)
|
||||
XCTAssertEqual(actualBlack, expectedBlack)
|
||||
|
||||
let actualRed = Color(hexString: "F00")
|
||||
let expectedRed = Color(red: 255/255, green: 0, blue: 0)
|
||||
XCTAssertEqual(actualRed, expectedRed)
|
||||
|
||||
let actualGreen = Color(hexString: "0F0")
|
||||
let expectedGreen = Color(red: 0, green: 1, blue: 0)
|
||||
XCTAssertEqual(actualGreen, expectedGreen)
|
||||
|
||||
let actualBlue = Color(hexString: "00F")
|
||||
let expectedBlue = Color(red: 0, green: 0, blue: 1)
|
||||
XCTAssertEqual(actualBlue, expectedBlue)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,6 @@ final class SwiftUIChartsTests: XCTestCase {
|
||||
}
|
||||
|
||||
static var allTests = [
|
||||
("testExample", testExample),
|
||||
("testExample", testExample)
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import XCTest
|
||||
#if !canImport(ObjectiveC)
|
||||
public func allTests() -> [XCTestCaseEntry] {
|
||||
return [
|
||||
testCase(SwiftUIChartsTests.allTests),
|
||||
testCase(SwiftUIChartsTests.allTests)
|
||||
]
|
||||
}
|
||||
#endif
|
||||
|
||||