Skip to content

Commit

Permalink
feat: new protocol for chained functions, and added support for expli… (
Browse files Browse the repository at this point in the history
#252)

* feat: new protocol for chained functions, and added support for explicit Y ranges. X coming as well

* feat: add new axis interface (#253)
  • Loading branch information
AppPear authored Oct 24, 2022
1 parent d7e9802 commit ebaaf81
Show file tree
Hide file tree
Showing 40 changed files with 730 additions and 379 deletions.
98 changes: 98 additions & 0 deletions Sources/SwiftUICharts/Base/Axis/AxisLabels.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
11 changes: 11 additions & 0 deletions Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsPosition.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

public enum AxisLabelsYPosition {
case leading
case trailing
}

public enum AxisLabelsXPosition {
case top
case bottom
}
11 changes: 11 additions & 0 deletions Sources/SwiftUICharts/Base/Axis/Model/AxisLabelsStyle.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
10 changes: 10 additions & 0 deletions Sources/SwiftUICharts/Base/Axis/Model/AxisLablesData.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
2 changes: 1 addition & 1 deletion Sources/SwiftUICharts/Base/Chart/ChartBase.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import SwiftUI

/// Protocol for any type of chart, to get access to underlying data
public protocol ChartBase {
public protocol ChartBase: View {
var chartData: ChartData { get }
}
55 changes: 44 additions & 11 deletions Sources/SwiftUICharts/Base/Chart/ChartData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,70 @@ import SwiftUI

/// An observable wrapper for an array of data for use in any chart
public class ChartData: ObservableObject {
@Published public var data: [(String, Double)] = []
@Published public var data: [(Double, Double)] = []
public var rangeY: ClosedRange<Double>?
public var rangeX: ClosedRange<Double>?

var points: [Double] {
data.map { $0.1 }
data.filter { rangeX?.contains($0.0) ?? true }.map { $0.1 }
}

var values: [String] {
data.map { $0.0 }
var values: [Double] {
data.filter { rangeX?.contains($0.0) ?? true }.map { $0.0 }
}

var normalisedPoints: [Double] {
let absolutePoints = points.map { abs($0) }
return points.map { $0 / (absolutePoints.max() ?? 1.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 normalisedRange: Double {
(normalisedPoints.max() ?? 0.0) - (normalisedPoints.min() ?? 0.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 {
(points.min() ?? 0.0) < 0
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]) {
self.data = data.map { ("", $0) }
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: [(String, Double)]) {
public init(_ data: [(Double, Double)], rangeY: ClosedRange<FloatLiteralType>? = nil) {
self.data = data
self.rangeY = rangeY
}

public init() {
Expand Down
10 changes: 10 additions & 0 deletions Sources/SwiftUICharts/Base/Common/ViewGeometry.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
15 changes: 15 additions & 0 deletions Sources/SwiftUICharts/Base/Common/ViewPreferenceKey.swift
Original file line number Diff line number Diff line change
@@ -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())
}
}
14 changes: 14 additions & 0 deletions Sources/SwiftUICharts/Base/Common/ViewSizeData.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
7 changes: 7 additions & 0 deletions Sources/SwiftUICharts/Base/Extensions/Array+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,10 @@ extension Array where Element == ColorGradient {
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
}
}
26 changes: 14 additions & 12 deletions Sources/SwiftUICharts/Base/Extensions/ChartBase+Extension.swift
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import SwiftUI

extension View where Self: ChartBase {

/// Set data for a chart
/// - Parameter data: array of `Double`
/// - Returns: modified `View` with data attached
public func data(_ data: [Double]) -> some View {
chartData.data = data.map { ("", $0) }
extension ChartBase {
public func data(_ data: [Double]) -> some ChartBase {
chartData.data = data.enumerated().map{ (index, value) in (Double(index), value) }
return self
.environmentObject(chartData)
.environmentObject(ChartValue())
}

public func data(_ data: [(String, Double)]) -> some View {
public func data(_ data: [(Double, Double)]) -> some ChartBase {
chartData.data = data
return self
.environmentObject(chartData)
.environmentObject(ChartValue())
}

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
}
}
Loading

0 comments on commit ebaaf81

Please sign in to comment.