Compare commits

..

29 Commits

Author SHA1 Message Date
Andras Samu 7140b8b6fd feat: add animation toggle interface (#256)
Co-authored-by: “Andras <“andras.samu@supercharge.io”>
2022-11-26 14:37:17 +01:00
Andras Samu 7fd5b185f8 Feat/new protocol and range (#255)
* feat: new protocol for chained functions, and added support for explicit Y ranges. X coming as well

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

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

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

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

* Dark/Light mode fixes (#148)

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

Also solves line chart background to appear white in dark mode

* Add custom string format for ChartLabel when interactionInProgress = true

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

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

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

* First pass on most/all files

* more descriptions filled in

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

* more basic stuff filled in

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

* more explanations

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

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

* Fixed appearing animation
2020-06-21 18:53:48 +02:00
Andras Samu f0eea58bd8 Add CardView and CardLabel (#111) 2020-05-30 18:07:49 +02:00
nicolas d64d0e9d7a Bug Fix: Bar Chart with [0] crashed (#110) 2020-05-30 17:18:01 +02:00
Andras Samu 0caebce9ff added mac os as a build target 2020-05-28 18:50:59 +02:00
nicolas f2866ae281 Syntax corrections (#105)
* Movie PieChartFile

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

* Add PieChart
Allow multi color for Pie and Bar
Add linter
2020-05-25 14:20:46 +02:00
Adrian a2d75dca0e Write unit tests for CGPoint+Extension.swift (#100)
- write unit tests for CGPoint+Extension.swift
- clean up formatting on CGPoint+Extension.swift
2020-05-25 09:36:47 +02:00
Andras Samu 5cd5858967 Added a first implementation of BarChart and LineChart also introduced style 2020-05-24 18:38:19 +02:00
Andras Samu d869e4186f Replace the old with a more sleek and flexible code architecture 2020-05-22 17:42:35 +02:00
83 changed files with 2330 additions and 1691 deletions
-34
View File
@@ -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:
-20
View File
@@ -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 -->
-11
View File
@@ -1,11 +0,0 @@
---
name: v2 ticket
about: Create tasks for the upcoming new version
title: ''
labels: v2
assignees: ''
---
# v2 ticket
## Ticket description:
-29
View File
@@ -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.
-22
View File
@@ -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
+64
View File
@@ -0,0 +1,64 @@
disabled_rules:
- explicit_acl
- trailing_whitespace
- force_cast
- unused_closure_parameter
- multiple_closures_with_trailing_closure
opt_in_rules:
- anyobject_protocol
- array_init
- attributes
- collection_alignment
- colon
- conditional_returns_on_newline
- convenience_type
- empty_count
- empty_string
- empty_collection_literal
- enum_case_associated_values_count
- function_default_parameter_at_end
- fatal_error_message
- file_name
- first_where
- modifier_order
- toggle_bool
- unused_private_declaration
- yoda_condition
excluded:
- Carthage
- Pods
- SwiftLint/Common/3rdPartyLib
identifier_name:
excluded:
- a
- b
- c
- i
- id
- t
- to
- x
- y
line_length:
warning: 150
error: 200
ignores_function_declarations: true
ignores_comments: true
ignores_urls: true
function_body_length:
warning: 300
error: 500
function_parameter_count:
warning: 6
error: 8
type_body_length:
warning: 300
error: 400
file_length:
warning: 500
error: 1200
ignore_comment_only_lines: true
cyclomatic_complexity:
warning: 15
error: 21
reporter: "xcode"
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>SwiftUICharts.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "91E23D30-CB6C-44DA-BEFC-9D39A1DA2242"
type = "1"
version = "2.0">
</Bucket>
@@ -7,7 +7,20 @@
<key>SwiftUICharts.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
<integer>0</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>SwiftUICharts</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>SwiftUIChartsTests</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
+2 -2
View File
@@ -12,7 +12,7 @@ let package = Package(
// 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"])
]
)
-9
View File
@@ -4,20 +4,11 @@ Swift package for displaying charts effortlessly.
![SwiftUI Charts](./Resources/showcase1.gif "SwiftUI Charts")
### Note:
**A version 2.0 is coming soon!!! 🎉🎉🎉**, so please hold off your PRs for a while. I'm writing a new code base with more sleek code architecture with an option for easier expansion. I'll make beta releases so you can test betas.
**If you'd like to contribute you can find tickets for the new version in the Issues under the `v2` tag, please read more at: [https://github.com/AppPear/ChartView/pull/89](https://github.com/AppPear/ChartView/pull/89)**
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-el5pnmba-FvyraEsI~EwxqWHNfHZWZg)
### Installation:
It requires iOS 13 and Xcode 11!
@@ -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,148 +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
@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"){
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!
}
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
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,98 @@
import SwiftUI
public struct AxisLabels<Content: View>: View {
struct YAxisViewKey: ViewPreferenceKey { }
struct ChartViewKey: ViewPreferenceKey { }
var axisLabelsData = AxisLabelsData()
var axisLabelsStyle = AxisLabelsStyle()
@State private var yAxisWidth: CGFloat = 25
@State private var chartWidth: CGFloat = 0
@State private var chartHeight: CGFloat = 0
let content: () -> Content
public init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var yAxis: some View {
VStack(spacing: 0.0) {
ForEach(Array(axisLabelsData.axisYLabels.reversed().enumerated()), id: \.element) { index, axisYData in
Text(axisYData)
.font(axisLabelsStyle.axisFont)
.foregroundColor(axisLabelsStyle.axisFontColor)
.frame(height: getYHeight(index: index,
chartHeight: chartHeight,
count: axisLabelsData.axisYLabels.count),
alignment: getYAlignment(index: index, count: axisLabelsData.axisYLabels.count))
}
}
.padding([.leading, .trailing], 4.0)
.background(ViewGeometry<YAxisViewKey>())
.onPreferenceChange(YAxisViewKey.self) { value in
yAxisWidth = value.first?.size.width ?? 0.0
}
}
func xAxis(chartWidth: CGFloat) -> some View {
HStack(spacing: 0.0) {
ForEach(Array(axisLabelsData.axisXLabels.enumerated()), id: \.element) { index, axisXData in
Text(axisXData)
.font(axisLabelsStyle.axisFont)
.foregroundColor(axisLabelsStyle.axisFontColor)
.frame(width: chartWidth / CGFloat(axisLabelsData.axisXLabels.count - 1))
}
}
.frame(height: 24.0, alignment: .top)
}
var chart: some View {
self.content()
.background(ViewGeometry<ChartViewKey>())
.onPreferenceChange(ChartViewKey.self) { value in
chartWidth = value.first?.size.width ?? 0.0
chartHeight = value.first?.size.height ?? 0.0
}
}
public var body: some View {
VStack(spacing: 0.0) {
HStack {
if axisLabelsStyle.axisLabelsYPosition == .leading {
yAxis
} else {
Spacer(minLength: yAxisWidth)
}
chart
if axisLabelsStyle.axisLabelsYPosition == .leading {
Spacer(minLength: yAxisWidth)
} else {
yAxis
}
}
xAxis(chartWidth: chartWidth)
}
}
private func getYHeight(index: Int, chartHeight: CGFloat, count: Int) -> CGFloat {
if index == 0 || index == count - 1 {
return chartHeight / (CGFloat(count - 1) * 2) + 10
}
return chartHeight / CGFloat(count - 1)
}
private func getYAlignment(index: Int, count: Int) -> Alignment {
if index == 0 {
return .top
}
if index == count - 1 {
return .bottom
}
return .center
}
}
@@ -0,0 +1,57 @@
import SwiftUI
extension AxisLabels {
public func setAxisYLabels(_ labels: [String],
position: AxisLabelsYPosition = .leading) -> AxisLabels {
self.axisLabelsData.axisYLabels = labels
self.axisLabelsStyle.axisLabelsYPosition = position
return self
}
public func setAxisXLabels(_ labels: [String]) -> AxisLabels {
self.axisLabelsData.axisXLabels = labels
return self
}
public func setAxisYLabels(_ labels: [(Double, String)],
range: ClosedRange<Int>,
position: AxisLabelsYPosition = .leading) -> AxisLabels {
let overreach = range.overreach + 1
var labelArray = [String](repeating: "", count: overreach)
labels.forEach {
let index = Int($0.0) - range.lowerBound
if labelArray[safe: index] != nil {
labelArray[index] = $0.1
}
}
self.axisLabelsData.axisYLabels = labelArray
self.axisLabelsStyle.axisLabelsYPosition = position
return self
}
public func setAxisXLabels(_ labels: [(Double, String)], range: ClosedRange<Int>) -> AxisLabels {
let overreach = range.overreach + 1
var labelArray = [String](repeating: "", count: overreach)
labels.forEach {
let index = Int($0.0) - range.lowerBound
if labelArray[safe: index] != nil {
labelArray[index] = $0.1
}
}
self.axisLabelsData.axisXLabels = labelArray
return self
}
public func setColor(_ color: Color) -> AxisLabels {
self.axisLabelsStyle.axisFontColor = color
return self
}
public func setFont(_ font: Font) -> AxisLabels {
self.axisLabelsStyle.axisFont = font
return self
}
}
@@ -0,0 +1,11 @@
import Foundation
public enum AxisLabelsYPosition {
case leading
case trailing
}
public enum AxisLabelsXPosition {
case top
case bottom
}
@@ -0,0 +1,11 @@
import SwiftUI
public final class AxisLabelsStyle: ObservableObject {
@Published public var axisFont: Font = .callout
@Published public var axisFontColor: Color = .primary
@Published var axisLabelsYPosition: AxisLabelsYPosition = .leading
@Published var axisLabelsXPosition: AxisLabelsXPosition = .bottom
public init() {
// no-op
}
}
@@ -0,0 +1,10 @@
import SwiftUI
public final class AxisLabelsData: ObservableObject {
@Published public var axisYLabels: [String] = []
@Published public var axisXLabels: [String] = []
public init() {
// no-op
}
}
@@ -0,0 +1,37 @@
import SwiftUI
/// View containing data and some kind of chart content
public struct CardView<Content: View>: View, ChartBase {
public var chartData = ChartData()
let content: () -> Content
private var showShadow: Bool
@EnvironmentObject var style: ChartStyle
/// Initialize with view options and a nested `ViewBuilder`
/// - Parameters:
/// - showShadow: should card have a rounded-rectangle shadow around it
/// - content: <#content description#>
public init(showShadow: Bool = true, @ViewBuilder content: @escaping () -> Content) {
self.showShadow = showShadow
self.content = content
}
/// The content and behavior of the `CardView`.
///
///
public var body: some View {
ZStack{
if showShadow {
RoundedRectangle(cornerRadius: 20)
.fill(Color.white)
.shadow(color: Color(white: 0.9, opacity: 1), radius: 8)
}
VStack (alignment: .leading) {
self.content()
}
.clipShape(RoundedRectangle(cornerRadius: showShadow ? 20 : 0))
}
}
}
@@ -0,0 +1,6 @@
import SwiftUI
/// Protocol for any type of chart, to get access to underlying data
public protocol ChartBase: View {
var chartData: ChartData { get }
}
@@ -0,0 +1,74 @@
import SwiftUI
/// An observable wrapper for an array of data for use in any chart
public class ChartData: ObservableObject {
@Published public var data: [(Double, Double)] = []
public var rangeY: ClosedRange<Double>?
public var rangeX: ClosedRange<Double>?
var points: [Double] {
data.filter { rangeX?.contains($0.0) ?? true }.map { $0.1 }
}
var values: [Double] {
data.filter { rangeX?.contains($0.0) ?? true }.map { $0.0 }
}
var normalisedPoints: [Double] {
let absolutePoints = points.map { abs($0) }
var maxPoint = absolutePoints.max()
if let rangeY = rangeY {
maxPoint = Double(rangeY.overreach)
return points.map { ($0 - rangeY.lowerBound) / (maxPoint ?? 1.0) }
}
return points.map { $0 / (maxPoint ?? 1.0) }
}
var normalisedValues: [Double] {
let absoluteValues = values.map { abs($0) }
var maxValue = absoluteValues.max()
if let rangeX = rangeX {
maxValue = Double(rangeX.overreach)
return values.map { ($0 - rangeX.lowerBound) / (maxValue ?? 1.0) }
}
return values.map { $0 / (maxValue ?? 1.0) }
}
var normalisedData: [(Double, Double)] {
Array(zip(normalisedValues, normalisedPoints))
}
var normalisedYRange: Double {
return rangeY == nil ? (normalisedPoints.max() ?? 0.0) - (normalisedPoints.min() ?? 0.0) : 1
}
var normalisedXRange: Double {
return rangeX == nil ? (normalisedValues.max() ?? 0.0) - (normalisedValues.min() ?? 0.0) : 1
}
var isInNegativeDomain: Bool {
if let rangeY = rangeY {
return rangeY.lowerBound < 0
}
return (points.min() ?? 0.0) < 0
}
/// Initialize with data array
/// - Parameter data: Array of `Double`
public init(_ data: [Double], rangeY: ClosedRange<FloatLiteralType>? = nil) {
self.data = data.enumerated().map{ (index, value) in (Double(index), value) }
self.rangeY = rangeY
}
public init(_ data: [(Double, Double)], rangeY: ClosedRange<FloatLiteralType>? = nil) {
self.data = data
self.rangeY = rangeY
}
public init() {
self.data = []
}
}
@@ -0,0 +1,7 @@
import SwiftUI
/// Representation of a single data point in a chart that is being observed
public class ChartValue: ObservableObject {
@Published var currentValue: Double = 0
@Published var interactionInProgress: Bool = false
}
@@ -0,0 +1,10 @@
import SwiftUI
public struct ViewGeometry<T>: View where T: PreferenceKey {
public var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: T.self, value: [ViewSizeData(size: geometry.size)] as! T.Value)
}
}
}
@@ -0,0 +1,15 @@
import SwiftUI
public protocol ViewPreferenceKey: PreferenceKey {
typealias Value = [ViewSizeData]
}
public extension ViewPreferenceKey {
static var defaultValue: [ViewSizeData] {
[]
}
static func reduce(value: inout [ViewSizeData], nextValue: () -> [ViewSizeData]) {
value.append(contentsOf: nextValue())
}
}
@@ -0,0 +1,14 @@
import SwiftUI
public struct ViewSizeData: Identifiable, Equatable, Hashable {
public let id: UUID = UUID()
public let size: CGSize
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
@@ -0,0 +1,26 @@
import Foundation
extension Array where Element == ColorGradient {
/// <#Description#>
/// - Parameter index: offset in data table
/// - Returns: <#description#>
func rotate(for index: Int) -> ColorGradient {
if self.isEmpty {
return ColorGradient.orangeBright
}
if self.count <= index {
return self[index % self.count]
}
return self[index]
}
}
extension Collection {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
@@ -0,0 +1,47 @@
import SwiftUI
extension CGPoint {
/// Calculate X and Y delta for each data point, based on data min/max and enclosing frame.
/// - Parameters:
/// - frame: Rectangle of enclosing frame
/// - data: array of `Double`
/// - Returns: X and Y delta as a `CGPoint`
static func getStep(frame: CGRect, data: [Double]) -> CGPoint {
let padding: CGFloat = 0
// stepWidth
var stepWidth: CGFloat = 0.0
if data.count < 2 {
stepWidth = 0.0
}
stepWidth = frame.size.width / CGFloat(data.count - 1)
// stepHeight
var stepHeight: CGFloat = 0.0
var min: Double?
var max: Double?
if let minPoint = data.min(), let maxPoint = data.max(), minPoint != maxPoint {
min = minPoint
max = maxPoint
} else {
return .zero
}
if let min = min, let max = max, min != max {
if min <= 0 {
stepHeight = (frame.size.height - padding) / CGFloat(max - min)
} else {
stepHeight = (frame.size.height - padding) / CGFloat(max + min)
}
}
return CGPoint(x: stepWidth, y: stepHeight)
}
func denormalize(with geometry: GeometryProxy) -> CGPoint {
let width = geometry.frame(in: .local).width
let height = geometry.frame(in: .local).height
return CGPoint(x: self.x * width, y: self.y * height)
}
}
@@ -0,0 +1,11 @@
import Foundation
import SwiftUI
extension CGRect {
/// Midpoint of rectangle
/// - Returns: the coordinate for a rectangle center
public var mid: CGPoint {
return CGPoint(x: self.midX, y: self.midY)
}
}
@@ -0,0 +1,23 @@
import SwiftUI
extension ChartBase {
public func data(_ data: [Double]) -> some ChartBase {
chartData.data = data.enumerated().map{ (index, value) in (Double(index), value) }
return self
}
public func data(_ data: [(Double, Double)]) -> some ChartBase {
chartData.data = data
return self
}
public func rangeY(_ range: ClosedRange<FloatLiteralType>) -> some ChartBase{
chartData.rangeY = range
return self
}
public func rangeX(_ range: ClosedRange<FloatLiteralType>) -> some ChartBase{
chartData.rangeX = range
return self
}
}
@@ -0,0 +1,25 @@
import SwiftUI
extension Color {
/// Create a `Color` from a hexadecimal representation
/// - Parameter hexString: 3, 6, or 8-character string, with optional (ignored) punctuation such as "#"
init(hexString: String) {
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int = UInt64()
Scanner(string: hex).scanHexInt64(&int)
let red, green, blue: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(red, green, blue) = ((int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(red, green, blue) = (int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
// FIXME: I think we need an an alpha value on this one. See link below.
// https://stackoverflow.com/a/56874327/4475605
(red, green, blue) = (int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(red, green, blue) = (0, 0, 0)
}
self.init(red: Double(red) / 255, green: Double(green) / 255, blue: Double(blue) / 255)
}
}
@@ -1,37 +1,30 @@
//
// File.swift
//
//
// Created by xspyhack on 2020/1/21.
//
import SwiftUI
extension Path {
func trimmedPath(for percent: CGFloat) -> Path {
// percent difference between points
let boundsDistance: CGFloat = 0.001
let completion: CGFloat = 1 - boundsDistance
let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent)
// Start/end points centered around given percentage, but capped if right at the very end
let start = pct > completion ? completion : pct - boundsDistance
let end = pct > completion ? 1 : pct + boundsDistance
return trimmedPath(from: start, to: end)
}
func point(for percent: CGFloat) -> CGPoint {
let path = trimmedPath(for: percent)
return CGPoint(x: path.boundingRect.midX, y: path.boundingRect.midY)
}
func point(to maxX: CGFloat) -> CGPoint {
let total = length
let sub = length(to: maxX)
let percent = sub / total
return point(for: percent)
}
var length: CGFloat {
var ret: CGFloat = 0.0
var start: CGPoint?
@@ -63,7 +56,7 @@ extension Path {
}
return ret
}
func length(to maxX: CGFloat) -> CGFloat {
var ret: CGFloat = 0.0
var start: CGPoint?
@@ -114,78 +107,153 @@ extension Path {
}
return ret
}
static func quadCurvedPathWithPoints(points:[Double], step:CGPoint, globalOffset: Double? = nil) -> Path {
var path = Path()
if (points.count < 2){
return path
}
let offset = globalOffset ?? points.min()!
// guard let offset = points.min() else { return path }
var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
path.move(to: p1)
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
}
return path
}
static func quadClosedCurvedPathWithPoints(points:[Double], step:CGPoint, globalOffset: Double? = nil) -> Path {
var path = Path()
if (points.count < 2){
return path
}
let offset = globalOffset ?? points.min()!
// guard let offset = points.min() else { return path }
static func quadCurvedPathWithPoints(points: [Double], step: CGPoint, globalOffset: Double? = nil) -> Path {
var path = Path()
if points.count < 2 {
return path
}
let offset = globalOffset ?? points.min()!
// guard let offset = points.min() else { return path }
var point1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
path.move(to: point1)
for pointIndex in 1..<points.count {
let point2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
let midPoint = CGPoint.midPointForPoints(firstPoint: point1, secondPoint: point2)
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point1))
path.addQuadCurve(to: point2, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point2))
point1 = point2
}
return path
}
static func quadCurvedPathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if data.count < 2 {
return path
}
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
var point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
path.move(to: point1)
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
let midPoint = CGPoint.midPointForPoints(firstPoint: point1, secondPoint: point2)
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point1))
path.addQuadCurve(to: point2, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point2))
point1 = point2
}
return path
}
static func drawChartMarkers(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
let filteredData = data.filter { $0.1 <= 1 && $0.1 >= 0 }
if filteredData.count < 1 {
return path
}
let convertedXValues = filteredData.map { CGFloat($0.0) * rect.width }
let convertedYPoints = filteredData.map { CGFloat($0.1) * rect.height }
let markerSize = CGSize(width: 8, height: 8)
for pointIndex in 0..<filteredData.count {
path.addRoundedRect(in: CGRect(origin: CGPoint(x: convertedXValues[pointIndex] - markerSize.width / 2,
y: convertedYPoints[pointIndex] - markerSize.height / 2),
size: markerSize),
cornerSize: CGSize(width: markerSize.width / 2,
height: markerSize.height / 2))
}
return path
}
static func drawGridLines(numberOfHorizontalLines: Int, numberOfVerticalLines: Int, in rect: CGRect) -> Path {
var path = Path()
for index in 0..<numberOfHorizontalLines {
let normalisedSpacing = 1.0 / CGFloat(numberOfHorizontalLines - 1)
let startPoint = CGPoint(x: 0, y: normalisedSpacing * CGFloat(index) * rect.height)
let endPoint = CGPoint(x: rect.width, y: normalisedSpacing * CGFloat(index) * rect.height)
path.move(to: startPoint)
path.addLine(to: endPoint)
}
for index in 0..<numberOfVerticalLines {
let normalisedSpacing = 1.0 / CGFloat(numberOfVerticalLines - 1)
let startPoint = CGPoint(x: normalisedSpacing * CGFloat(index) * rect.width, y: 0)
let endPoint = CGPoint(x: normalisedSpacing * CGFloat(index) * rect.width, y: rect.height)
path.move(to: startPoint)
path.addLine(to: endPoint)
}
return path
}
static func quadClosedCurvedPathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if data.count < 2 {
return path
}
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
path.move(to: CGPoint(x: convertedXValues[0], y: 0))
var point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
path.addLine(to: point1)
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
let midPoint = CGPoint.midPointForPoints(firstPoint: point1, secondPoint: point2)
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point1))
path.addQuadCurve(to: point2, control: CGPoint.controlPointForPoints(firstPoint: midPoint, secondPoint: point2))
point1 = point2
}
path.addLine(to: CGPoint(x: point1.x, y: 0))
path.closeSubpath()
return path
}
static func linePathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if data.count < 2 {
return path
}
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
let point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
path.move(to: point1)
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
path.addLine(to: point2)
}
return path
}
static func closedLinePathWithPoints(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if data.count < 2 {
return path
}
let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
path.move(to: .zero)
var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
path.addLine(to: p1)
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 point1 = CGPoint(x: convertedXValues[0], y: convertedYPoints[0])
path.addLine(to: point1)
for pointIndex in 1..<data.count {
let point2 = CGPoint(x: CGFloat(convertedXValues[pointIndex]), y: CGFloat(convertedYPoints[pointIndex]))
path.addLine(to: point2)
}
path.addLine(to: CGPoint(x: p1.x, y: 0))
path.closeSubpath()
return path
}
static func linePathWithPoints(points:[Double], step:CGPoint) -> Path {
var path = Path()
if (points.count < 2){
return path
}
guard let offset = points.min() else { return path }
let p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
path.move(to: p1)
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)
}
return path
}
static func closedLinePathWithPoints(points:[Double], step:CGPoint) -> Path {
var path = Path()
if (points.count < 2){
return path
}
guard let offset = points.min() else { return path }
var 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)
}
path.addLine(to: CGPoint(x: p1.x, y: 0))
path.addLine(to: CGPoint(x: point1.x, y: 0))
path.closeSubpath()
return path
}
@@ -197,15 +265,15 @@ extension CGPoint {
let y = self.y + (x - self.x) * a
return CGPoint(x: x, y: y)
}
func line(to: CGPoint) -> CGFloat {
dist(to: to)
}
func line(to: CGPoint, x: CGFloat) -> CGFloat {
dist(to: point(to: to, x: x))
}
func quadCurve(to: CGPoint, control: CGPoint) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
@@ -220,7 +288,7 @@ extension CGPoint {
}
return dist
}
func quadCurve(to: CGPoint, control: CGPoint, x: CGFloat) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
@@ -245,14 +313,14 @@ extension CGPoint {
}
return dist
}
func point(to: CGPoint, t: CGFloat, control: CGPoint) -> CGPoint {
let x = CGPoint.value(x: self.x, y: to.x, t: t, c: control.x)
let y = CGPoint.value(x: self.y, y: to.y, t: t, c: control.y)
return CGPoint(x: x, y: y)
}
func curve(to: CGPoint, control1: CGPoint, control2: CGPoint) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
@@ -269,7 +337,7 @@ extension CGPoint {
return dist
}
func curve(to: CGPoint, control1: CGPoint, control2: CGPoint, x: CGFloat) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100
@@ -296,14 +364,14 @@ extension CGPoint {
return dist
}
func point(to: CGPoint, t: CGFloat, control1: CGPoint, control2: CGPoint) -> CGPoint {
let x = CGPoint.value(x: self.x, y: to.x, t: t, 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)
}
static func value(x: CGFloat, y: CGFloat, t: CGFloat, c: CGFloat) -> CGFloat {
var value: CGFloat = 0.0
// (1-t)^2 * p0 + 2 * (1-t) * t * c1 + t^2 * p1
@@ -312,42 +380,43 @@ extension CGPoint {
value += pow(t, 2) * y
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
}
static func getMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint {
return CGPoint(
x: point1.x + (point2.x - point1.x) / 2,
y: point1.y + (point2.y - point1.y) / 2
)
}
func dist(to: CGPoint) -> CGFloat {
return sqrt((pow(self.x - to.x, 2) + pow(self.y - to.y, 2)))
}
static func midPointForPoints(p1:CGPoint, p2:CGPoint) -> CGPoint {
return CGPoint(x:(p1.x + p2.x) / 2,y: (p1.y + p2.y) / 2)
static func 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 Foundation
public extension ClosedRange where Bound: AdditiveArithmetic {
var overreach: Bound {
self.upperBound - self.lowerBound
}
}
@@ -0,0 +1,17 @@
import SwiftUI
extension Shape {
func fill<Fill: ShapeStyle, Stroke: ShapeStyle>(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View {
self
.stroke(strokeStyle, lineWidth: lineWidth)
.background(self.fill(fillStyle))
}
}
extension InsettableShape {
func fill<Fill: ShapeStyle, Stroke: ShapeStyle>(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View {
self
.strokeBorder(strokeStyle, lineWidth: lineWidth)
.background(self.fill(fillStyle))
}
}
@@ -0,0 +1,17 @@
import SwiftUI
extension View {
/// Attach chart style to a View
/// - Parameter style: chart style
/// - Returns: `View` with chart style attached
public func chartStyle(_ style: ChartStyle) -> some View {
self.environmentObject(style)
}
public func toStandardCoordinateSystem() -> some View {
self
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
}
}
@@ -0,0 +1,24 @@
import SwiftUI
public struct ChartGrid<Content: View>: View {
let content: () -> Content
public var gridOptions = GridOptions()
public init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
public var body: some View {
ZStack {
ChartGridShape(numberOfHorizontalLines: gridOptions.numberOfHorizontalLines,
numberOfVerticalLines: gridOptions.numberOfVerticalLines)
.stroke(gridOptions.color, style: gridOptions.strokeStyle)
if gridOptions.showBaseLine {
ChartGridBaseShape()
.stroke(gridOptions.color, style: gridOptions.baseStrokeStyle)
.rotationEffect(.degrees(180), anchor: .center)
}
self.content()
}
}
}
@@ -0,0 +1,18 @@
import SwiftUI
struct ChartGridBaseShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: 0))
return path
}
}
struct ChartGridBaseShape_Previews: PreviewProvider {
static var previews: some View {
ChartGridBaseShape()
.stroke()
.rotationEffect(.degrees(180), anchor: .center)
}
}
@@ -0,0 +1,28 @@
import SwiftUI
struct ChartGridShape: Shape {
var numberOfHorizontalLines: Int
var numberOfVerticalLines: Int
func path(in rect: CGRect) -> Path {
let path = Path.drawGridLines(numberOfHorizontalLines: numberOfHorizontalLines,
numberOfVerticalLines: numberOfVerticalLines,
in: rect)
return path
}
}
struct ChartGridShape_Previews: PreviewProvider {
static var previews: some View {
Group {
ChartGridShape(numberOfHorizontalLines: 5, numberOfVerticalLines: 0)
.stroke()
.toStandardCoordinateSystem()
ChartGridShape(numberOfHorizontalLines: 4, numberOfVerticalLines: 4)
.stroke()
.toStandardCoordinateSystem()
}
.padding()
}
}
@@ -0,0 +1,31 @@
import SwiftUI
extension ChartGrid {
public func setNumberOfHorizontalLines(_ numberOfLines: Int) -> ChartGrid {
self.gridOptions.numberOfHorizontalLines = numberOfLines
return self
}
public func setNumberOfVerticalLines(_ numberOfLines: Int) -> ChartGrid {
self.gridOptions.numberOfVerticalLines = numberOfLines
return self
}
public func setStoreStyle(_ strokeStyle: StrokeStyle) -> ChartGrid {
self.gridOptions.strokeStyle = strokeStyle
return self
}
public func setColor(_ color: Color) -> ChartGrid {
self.gridOptions.color = color
return self
}
public func showBaseLine(_ show: Bool, with style: StrokeStyle? = nil) -> ChartGrid {
self.gridOptions.showBaseLine = show
if let style = style {
self.gridOptions.baseStrokeStyle = style
}
return self
}
}
@@ -0,0 +1,14 @@
import SwiftUI
public final class GridOptions: ObservableObject {
@Published public var numberOfHorizontalLines: Int = 3
@Published public var numberOfVerticalLines: Int = 3
@Published public var strokeStyle: StrokeStyle = StrokeStyle(lineWidth: 1, lineCap: .round, dash: [5, 10])
@Published public var color: Color = Color(white: 0.85)
@Published public var showBaseLine: Bool = true
@Published public var baseStrokeStyle: StrokeStyle = StrokeStyle(lineWidth: 1.5, lineCap: .round, dash: [5, 0])
public init() {
// no-op
}
}
@@ -0,0 +1,107 @@
import SwiftUI
/// What kind of label - this affects color, size, position of the label
public enum ChartLabelType {
case title
case subTitle
case largeTitle
case custom(size: CGFloat, padding: EdgeInsets, color: Color)
case legend
}
/// A chart may contain any number of labels in pre-set positions based on their `ChartLabelType`
public struct ChartLabel: View {
@EnvironmentObject var chartValue: ChartValue
@State var textToDisplay:String = ""
var format: String = "%.01f"
private var title: String
/// Label font size
/// - Returns: the font size of the label
private var labelSize: CGFloat {
switch labelType {
case .title:
return 32.0
case .legend:
return 14.0
case .subTitle:
return 24.0
case .largeTitle:
return 38.0
case .custom(let size, _, _):
return size
}
}
/// Padding around label
/// - Returns: the edge padding to use based on position of the label
private var labelPadding: EdgeInsets {
switch labelType {
case .title:
return EdgeInsets(top: 16.0, leading: 0, bottom: 0.0, trailing: 8.0)
case .legend:
return EdgeInsets(top: 4.0, leading: 0, bottom: 0.0, trailing: 8.0)
case .subTitle:
return EdgeInsets(top: 8.0, leading: 0, bottom: 0.0, trailing: 8.0)
case .largeTitle:
return EdgeInsets(top: 24.0, leading: 0, bottom: 0.0, trailing: 8.0)
case .custom(_, let padding, _):
return padding
}
}
/// Which type (color, size, position) for label
private let labelType: ChartLabelType
/// Foreground color for this label
/// - Returns: Color of label based on its `ChartLabelType`
private var labelColor: Color {
switch labelType {
case .title:
return Color.primary
case .legend:
return Color.secondary
case .subTitle:
return Color.primary
case .largeTitle:
return Color.primary
case .custom(_, _, let color):
return color
}
}
/// Initialize
/// - Parameters:
/// - title: Any `String`
/// - type: Which `ChartLabelType` to use
public init (_ title: String,
type: ChartLabelType = .title,
format: String = "%.01f") {
self.title = title
labelType = type
self.format = format
}
/// The content and behavior of the `ChartLabel`.
///
/// Displays current value if chart is currently being touched along a data point, otherwise the specified text.
public var body: some View {
HStack {
Text(textToDisplay)
.font(.system(size: labelSize))
.bold()
.foregroundColor(self.labelColor)
.padding(self.labelPadding)
.onAppear {
self.textToDisplay = self.title
}
.onReceive(self.chartValue.objectWillChange) { _ in
self.textToDisplay = self.chartValue.interactionInProgress ? String(format: format, self.chartValue.currentValue) : self.title
}
if !self.chartValue.interactionInProgress {
Spacer()
}
}
}
}
@@ -0,0 +1,27 @@
import SwiftUI
public class ChartStyle: ObservableObject {
public let backgroundColor: ColorGradient
public let foregroundColor: [ColorGradient]
public init(backgroundColor: Color, foregroundColor: [ColorGradient]) {
self.backgroundColor = ColorGradient.init(backgroundColor)
self.foregroundColor = foregroundColor
}
public init(backgroundColor: Color, foregroundColor: ColorGradient) {
self.backgroundColor = ColorGradient.init(backgroundColor)
self.foregroundColor = [foregroundColor]
}
public init(backgroundColor: ColorGradient, foregroundColor: ColorGradient) {
self.backgroundColor = backgroundColor
self.foregroundColor = [foregroundColor]
}
public init(backgroundColor: ColorGradient, foregroundColor: [ColorGradient]) {
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
}
}
@@ -0,0 +1,33 @@
import SwiftUI
public struct ColorGradient: Equatable {
public let startColor: Color
public let endColor: Color
public init(_ color: Color) {
self.startColor = color
self.endColor = color
}
public init(_ startColor: Color, _ endColor: Color) {
self.startColor = startColor
self.endColor = endColor
}
public var gradient: Gradient {
return Gradient(colors: [startColor, endColor])
}
}
extension ColorGradient {
public func linearGradient(from startPoint: UnitPoint, to endPoint: UnitPoint) -> LinearGradient {
return LinearGradient(gradient: self.gradient, startPoint: startPoint, endPoint: endPoint)
}
}
extension ColorGradient {
public static let orangeBright = ColorGradient(ChartColors.orangeBright)
public static let redBlack = ColorGradient(.red, .black)
public static let greenRed = ColorGradient(.green, .red)
public static let whiteBlack = ColorGradient(.white, .black)
}
@@ -0,0 +1,9 @@
import SwiftUI
public enum ChartColors {
public static let orangeBright = Color(hexString: "#FF782C")
public static let orangeDark = Color(hexString: "#EC2301")
public static let legendColor: Color = Color(hexString: "#E8E7EA")
public static let indicatorKnob: Color = Color(hexString: "#FF57A6")
}
@@ -0,0 +1,13 @@
import SwiftUI
public struct BarChart: ChartBase {
public var chartData = ChartData()
@EnvironmentObject var style: ChartStyle
public var body: some View {
BarChartRow(chartData: chartData, style: style)
}
public init() {}
}
@@ -0,0 +1,52 @@
import SwiftUI
public struct BarChartCell: View {
var value: Double
var index: Int = 0
var gradientColor: ColorGradient
var touchLocation: CGFloat
@State private var didCellAppear: Bool = false
public init( value: Double,
index: Int = 0,
gradientColor: ColorGradient,
touchLocation: CGFloat) {
self.value = value
self.index = index
self.gradientColor = gradientColor
self.touchLocation = touchLocation
}
public var body: some View {
BarChartCellShape(value: didCellAppear ? value : 0.0)
.fill(gradientColor.linearGradient(from: .bottom, to: .top)) .onAppear {
self.didCellAppear = true
}
.onDisappear {
self.didCellAppear = false
}
.transition(.slide)
.animation(Animation.spring().delay(self.touchLocation < 0 || !didCellAppear ? Double(self.index) * 0.04 : 0))
}
}
struct BarChartCell_Previews: PreviewProvider {
static var previews: some View {
Group {
Group {
BarChartCell(value: 0, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
BarChartCell(value: 0.5, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
BarChartCell(value: 0.75, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat())
BarChartCell(value: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat())
}
Group {
BarChartCell(value: 1, gradientColor: ColorGradient.greenRed, touchLocation: CGFloat())
BarChartCell(value: 1, gradientColor: ColorGradient.whiteBlack, touchLocation: CGFloat())
BarChartCell(value: 1, gradientColor: ColorGradient(.purple), touchLocation: CGFloat())
}.environment(\.colorScheme, .dark)
}
}
}
@@ -0,0 +1,49 @@
import SwiftUI
struct BarChartCellShape: Shape, Animatable {
var value: Double
var cornerRadius: CGFloat = 6.0
var animatableData: CGFloat {
get { CGFloat(value) }
set { value = Double(newValue) }
}
func path(in rect: CGRect) -> Path {
let adjustedOriginY = rect.height - (rect.height * CGFloat(value))
var path = Path()
path.move(to: CGPoint(x: 0.0 , y: rect.height))
path.addLine(to: CGPoint(x: 0.0, y: adjustedOriginY + cornerRadius))
path.addArc(center: CGPoint(x: cornerRadius, y: adjustedOriginY + cornerRadius),
radius: cornerRadius,
startAngle: Angle(radians: Double.pi),
endAngle: Angle(radians: value < 0 ? Double.pi/2 : -Double.pi/2),
clockwise: value < 0 ? true : false)
path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: value < 0 ? adjustedOriginY + 2 * cornerRadius : adjustedOriginY))
path.addArc(center: CGPoint(x: rect.width - cornerRadius, y: adjustedOriginY + cornerRadius),
radius: cornerRadius,
startAngle: Angle(radians: value < 0 ? Double.pi/2 : -Double.pi/2),
endAngle: Angle(radians: 0),
clockwise: value < 0 ? true : false)
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.closeSubpath()
return path
}
}
struct BarChartCellShape_Previews: PreviewProvider {
static var previews: some View {
Group {
BarChartCellShape(value: 0.75)
.fill(Color.red)
BarChartCellShape(value: 0.3)
.fill(Color.blue)
BarChartCellShape(value: -0.3)
.fill(Color.blue)
.offset(x: 0, y: -600)
}
}
}
@@ -0,0 +1,70 @@
import SwiftUI
public struct BarChartRow: View {
@EnvironmentObject var chartValue: ChartValue
@ObservedObject var chartData: ChartData
@State private var touchLocation: CGFloat = -1.0
var style: ChartStyle
var maxValue: Double {
guard let max = chartData.points.max() else {
return 1
}
return max != 0 ? max : 1
}
public var body: some View {
GeometryReader { geometry in
HStack(alignment: .bottom,
spacing: geometry.frame(in: .local).width / CGFloat(chartData.data.count * 3)) {
ForEach(0..<chartData.data.count, id: \.self) { index in
BarChartCell(value: chartData.normalisedPoints[index],
index: index,
gradientColor: self.style.foregroundColor.rotate(for: index),
touchLocation: self.touchLocation)
.scaleEffect(self.getScaleSize(touchLocation: self.touchLocation, index: index), anchor: .bottom)
.animation(Animation.easeIn(duration: 0.2))
}
// .drawingGroup()
}
.frame(maxHeight: chartData.isInNegativeDomain ? geometry.size.height / 2 : geometry.size.height)
.gesture(DragGesture()
.onChanged({ value in
let width = geometry.frame(in: .local).width
self.touchLocation = value.location.x/width
if let currentValue = self.getCurrentValue(width: width) {
self.chartValue.currentValue = currentValue
self.chartValue.interactionInProgress = true
}
})
.onEnded({ value in
self.chartValue.interactionInProgress = false
self.touchLocation = -1
})
)
}
}
func getScaleSize(touchLocation: CGFloat, index: Int) -> CGSize {
if touchLocation > CGFloat(index)/CGFloat(chartData.data.count) &&
touchLocation < CGFloat(index+1)/CGFloat(chartData.data.count) {
return CGSize(width: 1.4, height: 1.1)
}
return CGSize(width: 1, height: 1)
}
func getCurrentValue(width: CGFloat) -> Double? {
guard self.chartData.data.count > 0 else { return nil}
let index = max(0,min(self.chartData.data.count-1,Int(floor((self.touchLocation*width)/(width/CGFloat(self.chartData.data.count))))))
return self.chartData.points[index]
}
}
struct BarChartRow_Previews: PreviewProvider {
static let chartData = ChartData([6, 2, 5, 8, 6])
static let chartStyle = ChartStyle(backgroundColor: .white, foregroundColor: .orangeBright)
static var previews: some View {
BarChartRow(chartData: chartData, style: chartStyle)
}
}
@@ -0,0 +1,29 @@
import SwiftUI
extension LineChart {
public func setLineWidth(width: CGFloat) -> LineChart {
self.chartProperties.lineWidth = width
return self
}
public func setBackground(colorGradient: ColorGradient) -> LineChart {
self.chartProperties.backgroundGradient = colorGradient
return self
}
public func showChartMarks(_ show: Bool, with color: ColorGradient? = nil) -> LineChart {
self.chartProperties.showChartMarks = show
self.chartProperties.customChartMarksColors = color
return self
}
public func setLineStyle(to style: LineStyle) -> LineChart {
self.chartProperties.lineStyle = style
return self
}
public func withAnimation(_ enabled: Bool) -> LineChart {
self.chartProperties.animationEnabled = enabled
return self
}
}
@@ -1,23 +1,15 @@
//
// IndicatorPoint.swift
// LineChart
//
// Created by András Samu on 2019. 09. 03..
// Copyright © 2019. András Samu. All rights reserved.
//
import SwiftUI
struct IndicatorPoint: View {
var body: some View {
ZStack{
public var body: some View {
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,111 @@
import SwiftUI
/// A single line of data, a view in a `LineChart`
public struct Line: View {
@ObservedObject var chartData: ChartData
@ObservedObject var chartProperties: LineChartProperties
var curvedLines: Bool = true
var style: ChartStyle
@State private var showIndicator: Bool = false
@State private var touchLocation: CGPoint = .zero
@State private var didCellAppear: Bool = false
var path: Path {
Path.quadCurvedPathWithPoints(points: chartData.normalisedPoints,
step: CGPoint(x: 1.0, y: 1.0))
}
public init(chartData: ChartData,
style: ChartStyle,
chartProperties: LineChartProperties) {
self.chartData = chartData
self.style = style
self.chartProperties = chartProperties
}
public var body: some View {
GeometryReader { geometry in
ZStack {
if self.didCellAppear, let backgroundColor = chartProperties.backgroundGradient {
LineBackgroundShapeView(chartData: chartData,
geometry: geometry,
backgroundColor: backgroundColor)
}
lineShapeView(geometry: geometry)
}
.onAppear {
didCellAppear = true
}
.onDisappear() {
didCellAppear = false
}
}
}
@ViewBuilder
private func lineShapeView(geometry: GeometryProxy) -> some View {
if chartProperties.animationEnabled {
LineShapeView(chartData: chartData,
chartProperties: chartProperties,
geometry: geometry,
style: style,
trimTo: didCellAppear ? 1.0 : 0.0)
.animation(Animation.easeIn(duration: 0.75))
} else {
LineShapeView(chartData: chartData,
chartProperties: chartProperties,
geometry: geometry,
style: style,
trimTo: 1.0)
}
}
}
// MARK: - Private functions
extension Line {
/// Calculate point closest to where the user touched
/// - Parameter touchLocation: location in view where touched
/// - Returns: `CGPoint` of data point on chart
private func getClosestPointOnPath(geometry: GeometryProxy, touchLocation: CGPoint) -> CGPoint {
let geometryWidth = geometry.frame(in: .local).width
let normalisedTouchLocationX = (touchLocation.x / geometryWidth) * CGFloat(chartData.normalisedPoints.count - 1)
let closest = self.path.point(to: normalisedTouchLocationX)
var denormClosest = closest.denormalize(with: geometry)
denormClosest.x = denormClosest.x / CGFloat(chartData.normalisedPoints.count - 1)
denormClosest.y = denormClosest.y / CGFloat(chartData.normalisedYRange)
return denormClosest
}
// /// Figure out where closest touch point was
// /// - Parameter point: location of data point on graph, near touch location
private func getClosestDataPoint(geometry: GeometryProxy, touchLocation: CGPoint) {
let geometryWidth = geometry.frame(in: .local).width
let index = Int(round((touchLocation.x / geometryWidth) * CGFloat(chartData.points.count - 1)))
if (index >= 0 && index < self.chartData.data.count){
// self.chartValue.currentValue = self.chartData.points[index]
}
}
}
struct Line_Previews: PreviewProvider {
/// Predefined style, black over white, for preview
static let blackLineStyle = ChartStyle(backgroundColor: ColorGradient(.white), foregroundColor: ColorGradient(.black))
/// Predefined style red over white, for preview
static let redLineStyle = ChartStyle(backgroundColor: .whiteBlack, foregroundColor: ColorGradient(.red))
static var previews: some View {
Group {
Line(chartData: ChartData([8, 23, 32, 7, 23, -4]),
style: blackLineStyle,
chartProperties: LineChartProperties())
Line(chartData: ChartData([8, 23, 32, 7, 23, 43]),
style: redLineStyle,
chartProperties: LineChartProperties())
}
}
}
@@ -0,0 +1,27 @@
import SwiftUI
struct LineBackgroundShape: Shape {
var data: [(Double, Double)]
func path(in rect: CGRect) -> Path {
let path = Path.quadClosedCurvedPathWithPoints(data: data, in: rect)
return path
}
}
struct LineBackgroundShape_Previews: PreviewProvider {
static var previews: some View {
Group {
GeometryReader { geometry in
LineBackgroundShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)])
.fill(Color.red)
.toStandardCoordinateSystem()
}
GeometryReader { geometry in
LineBackgroundShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)])
.fill(Color.blue)
.toStandardCoordinateSystem()
}
}
}
}
@@ -0,0 +1,16 @@
import SwiftUI
struct LineBackgroundShapeView: View {
var chartData: ChartData
var geometry: GeometryProxy
var backgroundColor: ColorGradient
var body: some View {
LineBackgroundShape(data: chartData.normalisedData)
.fill(LinearGradient(gradient: Gradient(colors: [backgroundColor.startColor,
backgroundColor.endColor]),
startPoint: .bottom,
endPoint: .top))
.toStandardCoordinateSystem()
}
}
@@ -0,0 +1,15 @@
import SwiftUI
public struct LineChart: ChartBase {
public var chartData = ChartData()
@EnvironmentObject var style: ChartStyle
public var chartProperties = LineChartProperties()
public var body: some View {
Line(chartData: chartData,
style: style,
chartProperties: chartProperties)
}
public init() {}
}
@@ -0,0 +1,30 @@
import SwiftUI
struct LineShape: Shape {
var data: [(Double, Double)]
var lineStyle: LineStyle = .curved
func path(in rect: CGRect) -> Path {
var path = Path()
switch lineStyle {
case .curved:
path = Path.quadCurvedPathWithPoints(data: data, in: rect)
case .straight:
path = Path.linePathWithPoints(data: data, in: rect)
}
return path
}
}
struct LineShape_Previews: PreviewProvider {
static var previews: some View {
Group {
LineShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)])
.stroke()
.toStandardCoordinateSystem()
LineShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)], lineStyle: .straight)
.stroke()
.toStandardCoordinateSystem()
}
}
}
@@ -0,0 +1,83 @@
import SwiftUI
struct LineShapeView: View, Animatable {
var chartData: ChartData
var chartProperties: LineChartProperties
var geometry: GeometryProxy
var style: ChartStyle
var trimTo: Double = 0
var animatableData: CGFloat {
get { CGFloat(trimTo) }
set { trimTo = Double(newValue) }
}
var chartMarkColor: LinearGradient {
if let customColor = chartProperties.customChartMarksColors {
return customColor.linearGradient(from: .leading, to: .trailing)
}
return LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient,
startPoint: .leading,
endPoint: .trailing)
}
var body: some View {
ZStack {
LineShape(data: chartData.normalisedData, lineStyle: chartProperties.lineStyle)
.trim(from: 0, to: CGFloat(trimTo))
.stroke(LinearGradient(gradient: style.foregroundColor.first?.gradient ?? ColorGradient.orangeBright.gradient,
startPoint: .leading,
endPoint: .trailing),
style: StrokeStyle(lineWidth: chartProperties.lineWidth, lineJoin: .round))
.toStandardCoordinateSystem()
.clipped()
if chartProperties.showChartMarks {
MarkerShape(data: chartData.normalisedData)
.trim(from: 0, to: CGFloat(trimTo))
.fill(.white,
strokeBorder: chartMarkColor,
lineWidth: chartProperties.lineWidth)
.toStandardCoordinateSystem()
}
}
}
}
struct LineShapeView_Previews: PreviewProvider {
static let chartData = ChartData([6, 8, 6], rangeY: 6...10)
static let chartDataOutOfRange = ChartData([-1, 8, 6, 12, 3], rangeY: -5...15)
static let chartDataOutOfRange2 = ChartData([6,6,8,5], rangeY: 5...10)
static let chartStyle = ChartStyle(backgroundColor: Color.white,
foregroundColor: [ColorGradient(Color.orange, Color.red)])
static var previews: some View {
Group {
GeometryReader { geometry in
LineShapeView(chartData: chartData,
chartProperties: LineChartProperties(),
geometry: geometry,
style: chartStyle,
trimTo: 1.0)
}
GeometryReader { geometry in
LineShapeView(chartData: chartDataOutOfRange,
chartProperties: LineChartProperties(),
geometry: geometry,
style: chartStyle,
trimTo: 1.0)
}
GeometryReader { geometry in
LineShapeView(chartData: chartDataOutOfRange2,
chartProperties: LineChartProperties(),
geometry: geometry,
style: chartStyle,
trimTo: 1.0)
}
}
}
}
@@ -0,0 +1,23 @@
import SwiftUI
struct MarkerShape: Shape {
var data: [(Double, Double)]
func path(in rect: CGRect) -> Path {
let path = Path.drawChartMarkers(data: data, in: rect)
return path
}
}
struct MarkerShape_Previews: PreviewProvider {
static var previews: some View {
Group {
MarkerShape(data: [(0, 0), (0.25, 0.5), (0.5,0.8), (0.75, 0.6), (1, 1)])
.stroke()
.toStandardCoordinateSystem()
MarkerShape(data: [(0, -0.5), (0.25, 0.8), (0.5,-0.6), (0.75,0.6), (1, 1)])
.stroke()
.toStandardCoordinateSystem()
}
}
}
@@ -0,0 +1,13 @@
import SwiftUI
public class LineChartProperties: ObservableObject {
@Published var lineWidth: CGFloat = 2.0
@Published var backgroundGradient: ColorGradient?
@Published var showChartMarks: Bool = true
@Published var customChartMarksColors: ColorGradient?
@Published var lineStyle: LineStyle = .curved
@Published var animationEnabled: Bool = true
public init() {
// no-op
}
}
@@ -0,0 +1,6 @@
import Foundation
public enum LineStyle {
case curved
case straight
}
@@ -0,0 +1,17 @@
import SwiftUI
/// A type of chart that displays a slice of "pie" for each data point
public struct PieChart: ChartBase {
public var chartData = ChartData()
@EnvironmentObject var style: ChartStyle
/// The content and behavior of the `PieChart`.
///
///
public var body: some View {
PieChartRow(chartData: chartData, style: style)
}
public init() {}
}
@@ -0,0 +1,116 @@
import SwiftUI
/// One slice of a `PieChartRow`
struct PieSlice: Identifiable {
var id = UUID()
var startDeg: Double
var endDeg: Double
var value: Double
}
/// A single row of data, a view in a `PieChart`
public struct PieChartCell: View {
@State private var show: Bool = false
var rect: CGRect
var radius: CGFloat {
return min(rect.width, rect.height)/2
}
var startDeg: Double
var endDeg: Double
/// Path representing this slice
var path: Path {
var path = Path()
path.addArc(
center: rect.mid,
radius: self.radius,
startAngle: Angle(degrees: self.startDeg),
endAngle: Angle(degrees: self.endDeg),
clockwise: false)
path.addLine(to: rect.mid)
path.closeSubpath()
return path
}
var index: Int
// Section line border color
var backgroundColor: Color
// Section color
var accentColor: ColorGradient
/// The content and behavior of the `PieChartCell`.
///
/// Fills and strokes with 2-pixel line (unless start/end degrees not yet set). Animates by scaling up to 100% when first appears.
public var body: some View {
Group {
path
.fill(self.accentColor.linearGradient(from: .bottom, to: .top))
.overlay(path.stroke(self.backgroundColor, lineWidth: (startDeg == 0 && endDeg == 0 ? 0 : 2)))
.scaleEffect(self.show ? 1 : 0)
.animation(Animation.spring().delay(Double(self.index) * 0.04))
.onAppear {
self.show = true
}
}
}
}
struct PieChartCell_Previews: PreviewProvider {
static var previews: some View {
Group {
GeometryReader { geometry in
PieChartCell(
rect: geometry.frame(in: .local),
startDeg: 00.0,
endDeg: 90.0,
index: 0,
backgroundColor: Color.red,
accentColor: ColorGradient.greenRed)
}.frame(width: 100, height: 100)
GeometryReader { geometry in
PieChartCell(
rect: geometry.frame(in: .local),
startDeg: 0.0,
endDeg: 90.0,
index: 0,
backgroundColor: Color.green,
accentColor: ColorGradient.redBlack)
}.frame(width: 100, height: 100)
GeometryReader { geometry in
PieChartCell(
rect: geometry.frame(in: .local),
startDeg: 100.0,
endDeg: 135.0,
index: 0,
backgroundColor: Color.black,
accentColor: ColorGradient.whiteBlack)
}.frame(width: 100, height: 100)
GeometryReader { geometry in
PieChartCell(
rect: geometry.frame(in: .local),
startDeg: 185.0,
endDeg: 290.0,
index: 1,
backgroundColor: Color.purple,
accentColor: ColorGradient(.purple))
}.frame(width: 100, height: 100)
GeometryReader { geometry in
PieChartCell(
rect: geometry.frame(in: .local),
startDeg: 0,
endDeg: 0,
index: 0,
backgroundColor: Color.purple,
accentColor: ColorGradient(.purple))
}.frame(width: 100, height: 100)
}.previewLayout(.fixed(width: 125, height: 125))
}
}
@@ -0,0 +1,34 @@
import SwiftUI
func isPointInCircle(point: CGPoint, circleRect: CGRect) -> Bool {
let r = min(circleRect.width, circleRect.height) / 2
let center = CGPoint(x: circleRect.midX, y: circleRect.midY)
let dx = point.x - center.x
let dy = point.y - center.y
let distance = sqrt(dx * dx + dy * dy)
return distance <= r
}
func degree(for point: CGPoint, inCircleRect circleRect: CGRect) -> Double {
let center = CGPoint(x: circleRect.midX, y: circleRect.midY)
let dx = point.x - center.x
let dy = point.y - center.y
let acuteDegree = Double(atan(dy / dx)) * (180 / .pi)
let isInBottomRight = dx >= 0 && dy >= 0
let isInBottomLeft = dx <= 0 && dy >= 0
let isInTopLeft = dx <= 0 && dy <= 0
let isInTopRight = dx >= 0 && dy <= 0
if isInBottomRight {
return acuteDegree
} else if isInBottomLeft {
return 180 - abs(acuteDegree)
} else if isInTopLeft {
return 180 + abs(acuteDegree)
} else if isInTopRight {
return 360 - abs(acuteDegree)
}
return 0
}
@@ -0,0 +1,69 @@
import SwiftUI
/// A single "row" (slice) of data, a view in a `PieChart`
public struct PieChartRow: View {
@ObservedObject var chartData: ChartData
@EnvironmentObject var chartValue: ChartValue
var style: ChartStyle
var slices: [PieSlice] {
var tempSlices: [PieSlice] = []
var lastEndDeg: Double = 0
let maxValue: Double = chartData.points.reduce(0, +)
for slice in chartData.points {
let normalized: Double = Double(slice) / (maxValue == 0 ? 1 : maxValue)
let startDeg = lastEndDeg
let endDeg = lastEndDeg + (normalized * 360)
lastEndDeg = endDeg
tempSlices.append(PieSlice(startDeg: startDeg, endDeg: endDeg, value: slice))
}
return tempSlices
}
@State private var currentTouchedIndex = -1 {
didSet {
if oldValue != currentTouchedIndex {
chartValue.interactionInProgress = currentTouchedIndex != -1
guard currentTouchedIndex != -1 else { return }
chartValue.currentValue = slices[currentTouchedIndex].value
}
}
}
public var body: some View {
GeometryReader { geometry in
ZStack {
ForEach(0..<self.slices.count) { index in
PieChartCell(
rect: geometry.frame(in: .local),
startDeg: self.slices[index].startDeg,
endDeg: self.slices[index].endDeg,
index: index,
backgroundColor: self.style.backgroundColor.startColor,
accentColor: self.style.foregroundColor.rotate(for: index)
)
.scaleEffect(currentTouchedIndex == index ? 1.1 : 1)
.animation(Animation.spring())
}
}
.gesture(DragGesture()
.onChanged({ value in
let rect = geometry.frame(in: .local)
let isTouchInPie = isPointInCircle(point: value.location, circleRect: rect)
if isTouchInPie {
let touchDegree = degree(for: value.location, inCircleRect: rect)
currentTouchedIndex = slices.firstIndex(where: { $0.startDeg < touchDegree && $0.endDeg > touchDegree }) ?? -1
} else {
currentTouchedIndex = -1
}
})
.onEnded({ value in
currentTouchedIndex = -1
})
)
}
}
}
@@ -0,0 +1,187 @@
//
// Ring.swift
// ChartViewV2Demo
//
// Created by Dan Wood on 8/20/20.
// Based on article and playground code by Frank Jia
// https://medium.com/@frankjia/creating-activity-rings-in-swiftui-11ef7d336676
import SwiftUI
extension Double {
func toRadians() -> Double {
return self * Double.pi / 180
}
func toCGFloat() -> CGFloat {
return CGFloat(self)
}
}
struct RingShape: Shape {
/// Helper function to convert percent values to angles in degrees
/// - Parameters:
/// - percent: percent, greater than 100 is OK
/// - startAngle: angle to add after converting
/// - Returns: angle in degrees
static func percentToAngle(percent: Double, startAngle: Double) -> Double {
(percent / 100 * 360) + startAngle
}
private var percent: Double
private var startAngle: Double
private let drawnClockwise: Bool
// This allows animations to run smoothly for percent values
var animatableData: Double {
get {
return percent
}
set {
percent = newValue
}
}
init(percent: Double = 100, startAngle: Double = -90, drawnClockwise: Bool = false) {
self.percent = percent
self.startAngle = startAngle
self.drawnClockwise = drawnClockwise
}
/// This draws a simple arc from the start angle to the end angle
///
/// - Parameter rect: The frame of reference for describing this shape.
/// - Returns: A path that describes this shape.
func path(in rect: CGRect) -> Path {
let width = rect.width
let height = rect.height
let radius = min(width, height) / 2
let center = CGPoint(x: width / 2, y: height / 2)
let endAngle = Angle(degrees: RingShape.percentToAngle(percent: self.percent, startAngle: self.startAngle))
return Path { path in
path.addArc(center: center, radius: radius, startAngle: Angle(degrees: startAngle), endAngle: endAngle, clockwise: drawnClockwise)
}
}
}
struct Ring: View {
private static let ShadowColor: Color = Color.black.opacity(0.2)
private static let ShadowRadius: CGFloat = 5
private static let ShadowOffsetMultiplier: CGFloat = ShadowRadius + 2
private let ringWidth: CGFloat
private let percent: Double
private let foregroundColor: ColorGradient
private let startAngle: Double = -90
private let touchLocation: CGFloat
private var gradientStartAngle: Double {
self.percent >= 100 ? relativePercentageAngle - 360 : startAngle
}
private var absolutePercentageAngle: Double {
RingShape.percentToAngle(percent: self.percent, startAngle: 0)
}
private var relativePercentageAngle: Double {
// Take into account the startAngle
absolutePercentageAngle + startAngle
}
private var lastGradientColor: Color {
self.foregroundColor.endColor
}
private var ringGradient: AngularGradient {
AngularGradient(
gradient: self.foregroundColor.gradient,
center: .center,
startAngle: Angle(degrees: self.gradientStartAngle),
endAngle: Angle(degrees: relativePercentageAngle)
)
}
init(ringWidth: CGFloat, percent: Double, foregroundColor: ColorGradient, touchLocation:CGFloat) {
self.ringWidth = ringWidth
self.percent = percent
self.foregroundColor = foregroundColor
self.touchLocation = touchLocation
}
var body: some View {
GeometryReader { geometry in
ZStack {
// Background for the ring. Use the final color with reduced opacity
RingShape()
.stroke(style: StrokeStyle(lineWidth: self.ringWidth))
.fill(lastGradientColor.opacity(0.142857))
// Foreground
RingShape(percent: self.percent, startAngle: self.startAngle)
.stroke(style: StrokeStyle(lineWidth: self.ringWidth, lineCap: .round))
.fill(self.ringGradient)
// End of ring with drop shadow
if self.getShowShadow(frame: geometry.size) {
Circle()
.fill(self.lastGradientColor)
.frame(width: self.ringWidth, height: self.ringWidth, alignment: .center)
.offset(x: self.getEndCircleLocation(frame: geometry.size).0,
y: self.getEndCircleLocation(frame: geometry.size).1)
.shadow(color: Ring.ShadowColor,
radius: Ring.ShadowRadius,
x: self.getEndCircleShadowOffset().0,
y: self.getEndCircleShadowOffset().1)
}
}
}
// Padding to ensure that the entire ring fits within the view size allocated
.padding(self.ringWidth / 2)
}
private func getEndCircleLocation(frame: CGSize) -> (CGFloat, CGFloat) {
// Get angle of the end circle with respect to the start angle
let angleOfEndInRadians: Double = relativePercentageAngle.toRadians()
let offsetRadius = min(frame.width, frame.height) / 2
return (offsetRadius * cos(angleOfEndInRadians).toCGFloat(), offsetRadius * sin(angleOfEndInRadians).toCGFloat())
}
private func getEndCircleShadowOffset() -> (CGFloat, CGFloat) {
let angleForOffset = absolutePercentageAngle + (self.startAngle + 90)
let angleForOffsetInRadians = angleForOffset.toRadians()
let relativeXOffset = cos(angleForOffsetInRadians)
let relativeYOffset = sin(angleForOffsetInRadians)
let xOffset = relativeXOffset.toCGFloat() * Ring.ShadowOffsetMultiplier
let yOffset = relativeYOffset.toCGFloat() * Ring.ShadowOffsetMultiplier
return (xOffset, yOffset)
}
private func getShowShadow(frame: CGSize) -> Bool {
if self.percent >= 100 {
return true
}
let circleRadius = min(frame.width, frame.height) / 2
let remainingAngleInRadians = (360 - absolutePercentageAngle).toRadians().toCGFloat()
return circleRadius * remainingAngleInRadians <= self.ringWidth
}
}
struct Ring_Previews: PreviewProvider {
static var previews: some View {
VStack {
Ring(
ringWidth: 50, percent: 5 ,
foregroundColor: ColorGradient(.green, .blue), touchLocation: -1.0
)
.frame(width: 200, height: 200)
Ring(
ringWidth: 20, percent: 110 ,
foregroundColor: ColorGradient(.red, .blue), touchLocation: -1.0
)
.frame(width: 200, height: 200)
}
}
}
@@ -0,0 +1,15 @@
import SwiftUI
public struct RingsChart: ChartBase {
public var chartData = ChartData()
@EnvironmentObject var style: ChartStyle
// TODO - should put background opacity, ring width & spacing as chart style values
public var body: some View {
RingsChartRow(width:10.0, spacing:5.0, chartData: chartData, style: style)
}
public init() {}
}
@@ -0,0 +1,133 @@
//
// RingsChartRow.swift
// ChartViewV2Demo
//
// Created by Dan Wood on 8/20/20.
//
import SwiftUI
public struct RingsChartRow: View {
var width : CGFloat
var spacing : CGFloat
@EnvironmentObject var chartValue: ChartValue
@ObservedObject var chartData: ChartData
@State var touchRadius: CGFloat = -1.0
var style: ChartStyle
public var body: some View {
GeometryReader { geometry in
ZStack {
// FIXME: Why is background circle offset strangely when frame isn't specified? See Preview below. Related to the .animation somehow ????
Circle()
.fill(RadialGradient(gradient: self.style.backgroundColor.gradient, center: .center, startRadius: min(geometry.size.width, geometry.size.height)/2.0, endRadius: 1.0))
ForEach(0..<self.chartData.data.count, id: \.self) { index in
let scaleUp = isRingScaled(size:geometry.size, touchRadius: self.touchRadius, index: index)
let scaledWidth = scaleUp ? self.width * 2.0 : self.width
let normalPadding = (width + spacing) * CGFloat(index)
let scaledDiff = (scaledWidth - width) / 2.0
let padding = min(normalPadding - scaledDiff,
min(geometry.size.width, geometry.size.height)/2.0 - width
// make sure it doesn't get to crazy value
)
Ring(ringWidth:scaledWidth, percent: self.chartData.points[index], foregroundColor:self.style.foregroundColor.rotate(for: index),
touchLocation: self.touchRadius)
.zIndex(scaleUp ? 1 : 0) // make sure zoomed one is on top
.padding(padding)
.animation(Animation.easeIn(duration: 0.5))
}
// .drawingGroup()
}
.gesture(DragGesture()
.onChanged({ value in
let frame = geometry.frame(in: .local)
let radius = min(frame.width, frame.height) / 2.0
let deltaX = value.location.x - frame.midX
let deltaY = value.location.y - frame.midY
self.touchRadius = sqrt(deltaX*deltaX + deltaY*deltaY) // Pythagorean equation
if let currentValue = self.getCurrentValue(maxRadius: radius) {
self.chartValue.currentValue = currentValue
self.chartValue.interactionInProgress = true
}
})
.onEnded({ value in
self.chartValue.interactionInProgress = false
self.touchRadius = -1
})
)
}
}
/// should we scale up the touched ring?
/// - Parameters:
/// - size: size of the view
/// - touchRadius: distance from center where touched
/// - index: which ring is being drawn
/// - Returns: size to scale up or just scale of 1 if not scaled up
func isRingScaled(size: CGSize, touchRadius: CGFloat, index: Int) -> Bool {
let radius = min(size.width, size.height) / 2.0
return index == self.touchedCircleIndex(maxRadius: radius)
}
/// Find which circle has been touched
/// - Parameter maxRadius: radius of overall view circle
/// - Returns: which circle index was touched, if found. 0 = outer, 1 = next one in, etc.
func touchedCircleIndex(maxRadius: CGFloat) -> Int? {
guard self.chartData.data.count > 0 else { return nil } // no data
// Pretend actual circle goes ½ the inter-ring spacing out, so that a touch
// is registered on either side of each ring
let radialDistanceFromEdge = (maxRadius + spacing/2) - self.touchRadius;
guard radialDistanceFromEdge >= 0 else { return nil } // touched outside of ring
let touchIndex = Int(floor(radialDistanceFromEdge / (width + spacing)))
if touchIndex >= self.chartData.data.count { return nil } // too far from outside, no ring
return touchIndex
}
/// Description
/// - Parameter maxRadius: radius of overall view circle
/// - Returns: percentage value of the touched circle, based on `touchRadius` if found
func getCurrentValue(maxRadius: CGFloat) -> Double? {
guard let index = self.touchedCircleIndex(maxRadius: maxRadius) else { return nil }
return self.chartData.points[index]
}
}
struct RingsChartRow_Previews: PreviewProvider {
static var previews: some View {
let multiStyle = ChartStyle(backgroundColor: ColorGradient(Color.black.opacity(0.05), Color.white),
foregroundColor:
[ColorGradient(.purple, .blue),
ColorGradient(.orange, .red),
ColorGradient(.green, .yellow),
])
return RingsChartRow(width:20.0, spacing:10.0, chartData: ChartData([25,50,75,100,125]), style: multiStyle)
// and why does this not get centered when frame isn't specified?
.frame(width:300, height:400)
}
}
-278
View File
@@ -1,278 +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)
}
#else
//iOS implementation
let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
static func playSelection() -> Void {
UISelectionFeedbackGenerator().selectionChanged()
}
#endif
}
@@ -1,99 +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
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: "%.2f")").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)
}
}
-107
View File
@@ -1,107 +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!
print(min,max)
}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
}
.drawingGroup()
if(self.showIndicator) {
IndicatorPoint()
.position(self.getClosestPointOnPath(touchLocation: self.touchLocation))
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
}
}
}
func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint {
let 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? = 14,
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 (self.rateValue ?? 0 != 0)
{
if (self.rateValue ?? 0 >= 0){
Image(systemName: "arrow.up")
}else{
Image(systemName: "arrow.down")
}
Text("\(self.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 + 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 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,132 +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
@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") {
self.data = ChartData(points: data)
self.title = title
self.legend = legend
self.style = style
self.valueSpecifier = valueSpecifier!
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)
.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)),
touchLocation: self.$indicatorLocation,
showIndicator: self.$hideHorizontalLines,
minDataValue: .constant(nil),
maxDataValue: .constant(nil),
showBackground: false,
gradient: self.style.gradientColor
)
.offset(x: 30, y: -20)
.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,33 +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)
}
}
}
}
@@ -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? = 14,
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 (self.rateValue >= 0){
Image(systemName: "arrow.up")
}else{
Image(systemName: "arrow.down")
}
Text("\(self.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
ZStack{
ForEach(0..<self.data.count) { 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,49 +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
}
public var body: some View {
GeometryReader { geometry in
ZStack{
ForEach(0..<self.slices.count){ i in
PieChartCell(rect: geometry.frame(in: .local), startDeg: self.slices[i].startDeg, endDeg: self.slices[i].endDeg, index: i, backgroundColor: self.backgroundColor,accentColor: self.accentColor)
}
}
}
}
}
#if DEBUG
struct PieChartRow_Previews : PreviewProvider {
static var previews: some View {
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)).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)).frame(width: 100, height: 100)
}
}
}
#endif
@@ -1,67 +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 init(data: [Double], title: String, legend: String? = nil, style: ChartStyle = Styles.pieChartStyleOne, form: CGSize? = ChartForm.medium, dropShadow: Bool? = true){
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!
}
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{
Text(self.title)
.font(.headline)
.foregroundColor(self.style.textColor)
Spacer()
Image(systemName: "chart.pie.fill")
.imageScale(.large)
.foregroundColor(self.style.legendTextColor)
}.padding()
PieChartRow(data: data, backgroundColor: self.style.backgroundColor, accentColor: self.style.accentColor)
.foregroundColor(self.style.accentColor).padding(self.legend != nil ? 0 : 12).offset(y:self.legend != nil ? 0 : -10)
if(self.legend != nil) {
Text(self.legend!)
.font(.headline)
.foregroundColor(self.style.legendTextColor)
.padding()
}
}
}.frame(width: self.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,37 @@
@testable import SwiftUICharts
import XCTest
class ArrayExtensionTests: XCTestCase {
func testArrayRotatingIndexEmpty() {
let colors = [ColorGradient]()
XCTAssertEqual(colors.rotate(for: 0), ColorGradient.orangeBright)
}
func testArrayRotatingIndexOneValue() {
let colors = [ColorGradient.greenRed]
XCTAssertEqual(colors.rotate(for: 0), ColorGradient.greenRed)
XCTAssertEqual(colors.rotate(for: 1), ColorGradient.greenRed)
XCTAssertEqual(colors.rotate(for: 2), ColorGradient.greenRed)
}
func testArrayRotatingIndexLessValues() {
let colors = [ColorGradient.greenRed, ColorGradient.whiteBlack]
XCTAssertEqual(colors.rotate(for: 0), ColorGradient.greenRed)
XCTAssertEqual(colors.rotate(for: 1), ColorGradient.whiteBlack)
XCTAssertEqual(colors.rotate(for: 2), ColorGradient.greenRed)
XCTAssertEqual(colors.rotate(for: 3), ColorGradient.whiteBlack)
XCTAssertEqual(colors.rotate(for: 4), ColorGradient.greenRed)
}
func testArrayRotatingIndexMoreValues() {
let colors = [ColorGradient.greenRed, ColorGradient.whiteBlack, ColorGradient.orangeBright]
XCTAssertEqual(colors.rotate(for: 0), ColorGradient.greenRed)
XCTAssertEqual(colors.rotate(for: 1), ColorGradient.whiteBlack)
}
}
@@ -0,0 +1,32 @@
//
// CGPointExtensionTests.swift
// SwiftUIChartsTests
//
// Created by Adrian Bolinger on 5/24/20.
//
@testable import SwiftUICharts
import XCTest
class CGPointExtensionTests: XCTestCase {
static let twentyElementArray: [Double] = Array(repeating: Double.random(in: 1...100), count: 20)
func testGetStepWithOneElementArray() {
let frame = CGRect(x: 0, y: 0, width: 300, height: 300)
let oneElementArray: [Double] = [0.0]
XCTAssertEqual(CGPoint.getStep(frame: frame, data: oneElementArray), .zero)
}
func testGetStepWithMultiElementArrayWithNegativeValues() {
let frame = CGRect(x: 0, y: 0, width: 300, height: 300)
let multiElementArray: [Double] = [-5.0, 0.0, 5.0]
XCTAssertEqual(CGPoint.getStep(frame: frame, data: multiElementArray), CGPoint(x: 150.0, y: 27.0))
}
func testGetStepWithMultiElementArrayWithPositiveValues() {
let frame = CGRect(x: 0, y: 0, width: 300, height: 300)
let multiElementArray: [Double] = [5.0, 10.0, 15.0]
XCTAssertEqual(CGPoint.getStep(frame: frame, data: multiElementArray), CGPoint(x: 150.0, y: 13.5))
}
}
@@ -0,0 +1,56 @@
//
// ColorExtensionTests.swift
// SwiftUIChartsTests
//
// Created by Adrian Bolinger on 5/24/20.
//
@testable import SwiftUICharts
import SwiftUI
import XCTest
class ColorExtensionTests: XCTestCase {
func testTwentyFourBitRGBColors() {
let actualWhite = Color(hexString: "FFFFFF")
let expectedWhite = Color(red: 1, green: 1, blue: 1)
XCTAssertEqual(actualWhite, expectedWhite)
let actualBlack = Color(hexString: "000000")
let expectedBlack = Color(red: 0, green: 0, blue: 0)
XCTAssertEqual(actualBlack, expectedBlack)
let actualRed = Color(hexString: "FF0000")
let expectedRed = Color(red: 255/255, green: 0, blue: 0)
XCTAssertEqual(actualRed, expectedRed)
let actualGreen = Color(hexString: "00FF00")
let expectedGreen = Color(red: 0, green: 1, blue: 0)
XCTAssertEqual(actualGreen, expectedGreen)
let actualBlue = Color(hexString: "0000FF")
let expectedBlue = Color(red: 0, green: 0, blue: 1)
XCTAssertEqual(actualBlue, expectedBlue)
}
func testTwelveBitRGBColors() {
let actualWhite = Color(hexString: "FFF")
let expectedWhite = Color(red: 1, green: 1, blue: 1)
XCTAssertEqual(actualWhite, expectedWhite)
let actualBlack = Color(hexString: "000")
let expectedBlack = Color(red: 0, green: 0, blue: 0)
XCTAssertEqual(actualBlack, expectedBlack)
let actualRed = Color(hexString: "F00")
let expectedRed = Color(red: 255/255, green: 0, blue: 0)
XCTAssertEqual(actualRed, expectedRed)
let actualGreen = Color(hexString: "0F0")
let expectedGreen = Color(red: 0, green: 1, blue: 0)
XCTAssertEqual(actualGreen, expectedGreen)
let actualBlue = Color(hexString: "00F")
let expectedBlue = Color(red: 0, green: 0, blue: 1)
XCTAssertEqual(actualBlue, expectedBlue)
}
}
@@ -9,6 +9,6 @@ final class SwiftUIChartsTests: XCTestCase {
}
static var allTests = [
("testExample", testExample),
("testExample", testExample)
]
}
@@ -3,7 +3,7 @@ import XCTest
#if !canImport(ObjectiveC)
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(SwiftUIChartsTests.allTests),
testCase(SwiftUIChartsTests.allTests)
]
}
#endif