Skip to content

Commit

Permalink
feat: add new axis interface
Browse files Browse the repository at this point in the history
  • Loading branch information
AppPear committed Oct 17, 2022
1 parent b48b701 commit 0034260
Show file tree
Hide file tree
Showing 14 changed files with 235 additions and 57 deletions.
104 changes: 81 additions & 23 deletions Sources/SwiftUICharts/Base/Axis/AxisLabels.swift
Original file line number Diff line number Diff line change
@@ -1,40 +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
// font
// foregroundColor

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 {
HStack {
VStack {
ForEach(Array(axisLabelsData.axisYLabels.reversed().enumerated()), id: \.element) { index, axisYData in
Text(axisYData)
if index != axisLabelsData.axisYLabels.count - 1 {
Spacer()
}
VStack(spacing: 0.0) {
HStack {
if axisLabelsStyle.axisLabelsYPosition == .leading {
yAxis
} else {
Spacer(minLength: yAxisWidth)
}
}
.padding([.trailing], 8.0)
.padding([.bottom], axisLabelsData.axisXLabels.count > 0 ? 28.0 : 0)
.frame(maxHeight: .infinity)
VStack {
self.content()
HStack {
ForEach(Array(axisLabelsData.axisXLabels.enumerated()), id: \.element) { index, axisXData in
Text(axisXData)
if index != axisLabelsData.axisXLabels.count - 1 {
Spacer()
}
}
chart
if axisLabelsStyle.axisLabelsYPosition == .leading {
Spacer(minLength: yAxisWidth)
} else {
yAxis
}
}
.padding([.top, .bottom], 10.0)
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
@@ -1,13 +1,57 @@
import SwiftUI

extension AxisLabels {
public func setAxisYLabels(_ labels: [String]) -> 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/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
}
}
10 changes: 6 additions & 4 deletions Sources/SwiftUICharts/Base/Extensions/Path+QuadCurve.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,17 @@ extension Path {

static func drawChartMarkers(data: [(Double, Double)], in rect: CGRect) -> Path {
var path = Path()
if data.count < 1 {
let filteredData = data.filter { $0.1 <= 1 && $0.1 >= 0 }

if filteredData.count < 1 {
return path
}

let convertedXValues = data.map { CGFloat($0.0) * rect.width }
let convertedYPoints = data.map { CGFloat($0.1) * rect.height }
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..<data.count {
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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ extension LineChart {
return self
}

public func showBackground(_ show: Bool) -> LineChart {
self.chartProperties.showBackground = show
public func setBackground(colorGradient: ColorGradient) -> LineChart {
self.chartProperties.backgroundGradient = colorGradient
return self
}

public func showChartMarks(_ show: Bool) -> LineChart {
public func showChartMarks(_ show: Bool, with color: ColorGradient? = nil) -> LineChart {
self.chartProperties.showChartMarks = show
self.chartProperties.customChartMarksColors = color
return self
}

Expand Down
34 changes: 15 additions & 19 deletions Sources/SwiftUICharts/Charts/LineChart/Line.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import SwiftUI

/// A single line of data, a view in a `LineChart`
public struct Line: View {
@EnvironmentObject var chartValue: ChartValue
@ObservedObject var chartData: ChartData
@ObservedObject var chartProperties: LineChartProperties

Expand All @@ -29,17 +28,17 @@ public struct Line: View {
public var body: some View {
GeometryReader { geometry in
ZStack {
if self.didCellAppear && self.chartProperties.showBackground {
if self.didCellAppear, let backgroundColor = chartProperties.backgroundGradient {
LineBackgroundShapeView(chartData: chartData,
geometry: geometry,
style: style)
backgroundColor: backgroundColor)
}
LineShapeView(chartData: chartData,
chartProperties: chartProperties,
geometry: geometry,
style: style,
trimTo: didCellAppear ? 1.0 : 0.0)
.animation(.easeIn)
.animation(Animation.easeIn(duration: 0.75))
if self.showIndicator {
IndicatorPoint()
.position(self.getClosestPointOnPath(geometry: geometry,
Expand All @@ -54,20 +53,17 @@ public struct Line: View {
.onDisappear() {
didCellAppear = false
}

.gesture(DragGesture()
.onChanged({ value in
self.touchLocation = value.location
self.showIndicator = true
self.getClosestDataPoint(geometry: geometry, touchLocation: value.location)
self.chartValue.interactionInProgress = true
})
.onEnded({ value in
self.touchLocation = .zero
self.showIndicator = false
self.chartValue.interactionInProgress = false
})
)
// .gesture(DragGesture()
// .onChanged({ value in
// self.touchLocation = value.location
// self.showIndicator = true
// self.getClosestDataPoint(geometry: geometry, touchLocation: value.location)
// })
// .onEnded({ value in
// self.touchLocation = .zero
// self.showIndicator = false
// })
// )
}
}
}
Expand All @@ -94,7 +90,7 @@ extension Line {
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]
// self.chartValue.currentValue = self.chartData.points[index]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import SwiftUI
struct LineBackgroundShapeView: View {
var chartData: ChartData
var geometry: GeometryProxy
var style: ChartStyle
var backgroundColor: ColorGradient

var body: some View {
LineBackgroundShape(data: chartData.normalisedData)
.fill(LinearGradient(gradient: Gradient(colors: [style.backgroundColor.startColor,
style.foregroundColor.first?.startColor ?? .white]),
.fill(LinearGradient(gradient: Gradient(colors: [backgroundColor.startColor,
backgroundColor.endColor]),
startPoint: .bottom,
endPoint: .top))
.rotationEffect(.degrees(180), anchor: .center)
Expand Down

0 comments on commit 0034260

Please sign in to comment.