Compare commits

...

3 Commits

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

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

Also solves line chart background to appear white in dark mode

* Add custom string format for ChartLabel when interactionInProgress = true

Co-authored-by: Sagar Patel <s.72427patel@gmail.com>
2020-08-24 16:44:05 +02:00
Andras Samu 9a588ebe5e Add PieChart interaction PR changes to v2 2020-08-24 16:43:08 +02:00
10 changed files with 115 additions and 93 deletions
@@ -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]
}
}