Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 422c5c0303 | |||
| c843e6bede | |||
| 9a588ebe5e |
@@ -2,11 +2,23 @@ import SwiftUI
|
||||
|
||||
/// An observable wrapper for an array of data for use in any chart
|
||||
public class ChartData: ObservableObject {
|
||||
@Published public var data: [Double] = []
|
||||
@Published public var data: [(String, Double)] = []
|
||||
|
||||
var points: [Double] {
|
||||
data.map { $0.1 }
|
||||
}
|
||||
|
||||
var values: [String] {
|
||||
data.map { $0.0 }
|
||||
}
|
||||
|
||||
/// Initialize with data array
|
||||
/// - Parameter data: Array of `Double`
|
||||
public init(_ data: [Double]) {
|
||||
self.data = data.map { ("", $0) }
|
||||
}
|
||||
|
||||
public init(_ data: [(String, Double)]) {
|
||||
self.data = data
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,13 @@ extension View where Self: ChartBase {
|
||||
/// - Parameter data: array of `Double`
|
||||
/// - Returns: modified `View` with data attached
|
||||
public func data(_ data: [Double]) -> some View {
|
||||
chartData.data = data.map { ("", $0) }
|
||||
return self
|
||||
.environmentObject(chartData)
|
||||
.environmentObject(ChartValue())
|
||||
}
|
||||
|
||||
public func data(_ data: [(String, Double)]) -> some View {
|
||||
chartData.data = data
|
||||
return self
|
||||
.environmentObject(chartData)
|
||||
|
||||
@@ -12,7 +12,8 @@ public enum ChartLabelType {
|
||||
/// 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 private var textToDisplay:String = ""
|
||||
@State var textToDisplay:String = ""
|
||||
var format: String = "%.01f"
|
||||
|
||||
private var title: String
|
||||
|
||||
@@ -74,10 +75,12 @@ public struct ChartLabel: View {
|
||||
/// - Parameters:
|
||||
/// - title: Any `String`
|
||||
/// - type: Which `ChartLabelType` to use
|
||||
public init(_ title: String,
|
||||
type: ChartLabelType = .title) {
|
||||
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`.
|
||||
@@ -94,7 +97,7 @@ public struct ChartLabel: View {
|
||||
self.textToDisplay = self.title
|
||||
}
|
||||
.onReceive(self.chartValue.objectWillChange) { _ in
|
||||
self.textToDisplay = self.chartValue.interactionInProgress ? String(format: "%.01f", self.chartValue.currentValue) : self.title
|
||||
self.textToDisplay = self.chartValue.interactionInProgress ? String(format: format, self.chartValue.currentValue) : self.title
|
||||
}
|
||||
if !self.chartValue.interactionInProgress {
|
||||
Spacer()
|
||||
|
||||
@@ -13,7 +13,7 @@ public struct BarChartRow: View {
|
||||
var style: ChartStyle
|
||||
|
||||
var maxValue: Double {
|
||||
guard let max = chartData.data.max() else {
|
||||
guard let max = chartData.points.max() else {
|
||||
return 1
|
||||
}
|
||||
return max != 0 ? max : 1
|
||||
@@ -62,7 +62,7 @@ public struct BarChartRow: View {
|
||||
/// - Parameter index: index into array of data
|
||||
/// - Returns: data value at given index, divided by data maximum
|
||||
func normalizedValue(index: Int) -> Double {
|
||||
return Double(chartData.data[index])/Double(maxValue)
|
||||
return Double(chartData.points[index])/Double(maxValue)
|
||||
}
|
||||
|
||||
/// Size to scale the touch indicator
|
||||
@@ -84,6 +84,6 @@ public struct BarChartRow: View {
|
||||
func getCurrentValue(width: CGFloat) -> Double? {
|
||||
guard self.chartData.data.count > 0 else { return nil}
|
||||
let index = max(0,min(self.chartData.data.count-1,Int(floor((self.touchLocation*width)/(width/CGFloat(self.chartData.data.count))))))
|
||||
return self.chartData.data[index]
|
||||
return self.chartData.points[index]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
//
|
||||
// IndicatorPoint.swift
|
||||
// LineChart
|
||||
//
|
||||
// Created by András Samu on 2019. 09. 03..
|
||||
// Copyright © 2019. András Samu. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A dot representing a single data point as user moves finger over line in `LineChart`
|
||||
|
||||
@@ -16,14 +16,14 @@ public struct Line: View {
|
||||
|
||||
/// Step for plotting through data
|
||||
/// - Returns: X and Y delta between each data point based on data and view's frame
|
||||
var step: CGPoint {
|
||||
return CGPoint.getStep(frame: frame, data: chartData.data)
|
||||
var step: CGPoint {
|
||||
return CGPoint.getStep(frame: frame, data: chartData.points)
|
||||
}
|
||||
|
||||
/// Path of line graph
|
||||
/// - Returns: A path for stroking representing the data, either curved or jagged.
|
||||
var path: Path {
|
||||
let points = chartData.data
|
||||
let points = chartData.points
|
||||
|
||||
if curvedLines {
|
||||
return Path.quadCurvedPathWithPoints(points: points,
|
||||
@@ -37,7 +37,7 @@ public struct Line: View {
|
||||
/// Path of linegraph, but also closed at the bottom side
|
||||
/// - Returns: A path for filling representing the data, either curved or jagged
|
||||
var closedPath: Path {
|
||||
let points = chartData.data
|
||||
let points = chartData.points
|
||||
|
||||
if curvedLines {
|
||||
return Path.quadClosedCurvedPathWithPoints(points: points,
|
||||
@@ -47,20 +47,19 @@ public struct Line: View {
|
||||
|
||||
return Path.closedLinePathWithPoints(points: points, step: step)
|
||||
}
|
||||
|
||||
// see https://stackoverflow.com/a/62370919
|
||||
// This lets geometry be recalculated when device rotates. However it doesn't cover issue of app changing
|
||||
// from full screen to split view. Not possible in SwiftUI? Feedback submitted to apple FB8451194.
|
||||
let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
|
||||
.makeConnectable()
|
||||
.autoconnect()
|
||||
|
||||
/// The content and behavior of the `Line`.
|
||||
///
|
||||
/// Draw the background if showing the full line (?) and the `showBackground` option is set. Above that draw the line, and then the data indicator if the graph is currently being touched.
|
||||
/// On appear, set the frame so that the data graph metrics can be calculated. On a drag (touch) gesture, highlight the closest touched data point.
|
||||
/// TODO: explain rotation
|
||||
public var body: some View {
|
||||
|
||||
let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
|
||||
.makeConnectable()
|
||||
.autoconnect() // see https://stackoverflow.com/a/62370919
|
||||
// This lets geometry be recalculated when device rotates. However it doesn't cover issue of app changing
|
||||
// from full screen to split view. Not possible in SwiftUI? Feedback submitted to apple FB8451194.
|
||||
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
if self.showFull && self.showBackground {
|
||||
@@ -120,7 +119,7 @@ extension Line {
|
||||
private func getClosestDataPoint(point: CGPoint) {
|
||||
let index = Int(round((point.x)/step.x))
|
||||
if (index >= 0 && index < self.chartData.data.count){
|
||||
self.chartValue.currentValue = self.chartData.data[index]
|
||||
self.chartValue.currentValue = self.chartData.points[index]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
//
|
||||
// PieChartCell.swift
|
||||
// SwiftUICharts
|
||||
//
|
||||
// Created by Nicolas Savoini on 2020-05-24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// One slice of a `PieChartRow`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,24 +1,18 @@
|
||||
//
|
||||
// PieChartRow.swift
|
||||
// SwiftUICharts
|
||||
//
|
||||
// Created by Nicolas Savoini on 2020-05-24.
|
||||
//
|
||||
|
||||
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.data.reduce(0, +)
|
||||
let maxValue: Double = chartData.points.reduce(0, +)
|
||||
|
||||
for slice in chartData.data {
|
||||
for slice in chartData.points {
|
||||
let normalized: Double = Double(slice) / (maxValue == 0 ? 1 : maxValue)
|
||||
let startDeg = lastEndDeg
|
||||
let endDeg = lastEndDeg + (normalized * 360)
|
||||
@@ -29,59 +23,47 @@ public struct PieChartRow: View {
|
||||
return tempSlices
|
||||
}
|
||||
|
||||
/// The content and behavior of the `PieChartRow`.
|
||||
///
|
||||
///
|
||||
@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)
|
||||
)
|
||||
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
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct PieChartRow_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
//Empty Array - Default Colors.OrangeStart
|
||||
PieChartRow(
|
||||
chartData: ChartData([8, 23, 32, 7, 23, 43]),
|
||||
style: defaultMultiColorChartStyle)
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
PieChartRow(
|
||||
chartData: ChartData([8, 23, 32, 7, 23, 43]),
|
||||
style: multiColorChartStyle)
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
PieChartRow(
|
||||
chartData: ChartData([8, 23, 32, 7, 23, 43]),
|
||||
style: multiColorChartStyle)
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
}.previewLayout(.fixed(width: 125, height: 125))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// Predefined color style, for preview
|
||||
private let defaultMultiColorChartStyle = ChartStyle(
|
||||
backgroundColor: Color.white,
|
||||
foregroundColor: [ColorGradient]())
|
||||
|
||||
/// Predefined color style, for preview
|
||||
private let multiColorChartStyle = ChartStyle(
|
||||
backgroundColor: Color.purple,
|
||||
foregroundColor: [ColorGradient.greenRed, ColorGradient.whiteBlack])
|
||||
|
||||
@@ -39,7 +39,7 @@ public struct RingsChartRow: View {
|
||||
// make sure it doesn't get to crazy value
|
||||
)
|
||||
|
||||
Ring(ringWidth:scaledWidth, percent: self.chartData.data[index], foregroundColor:self.style.foregroundColor.rotate(for: index),
|
||||
Ring(ringWidth:scaledWidth, percent: self.chartData.points[index], foregroundColor:self.style.foregroundColor.rotate(for: index),
|
||||
touchLocation: self.touchRadius)
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ public struct RingsChartRow: View {
|
||||
func getCurrentValue(maxRadius: CGFloat) -> Double? {
|
||||
|
||||
guard let index = self.touchedCircleIndex(maxRadius: maxRadius) else { return nil }
|
||||
return self.chartData.data[index]
|
||||
return self.chartData.points[index]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user