Skip to content

Commit 74a1b22

Browse files
ay42Sn0wfreezeDevborisyurkevich
authored
Merging Contributions (#23)
* Update (#19) (#20) * Adding a Bar chart style * Allowing negative values in the column chart Co-authored-by: Alexander Heinrich <alexander@alex-heinrich.dev> Co-authored-by: Alexander Heinrich <git@alex-heinrich.dev> * Add corner radius (#22) * Update BarChartStyle.swift * cleanup * Update README.md * Update Chart.swift Co-authored-by: Alexander Heinrich <git@alex-heinrich.dev> Co-authored-by: Boris Yurkevich <boris@cocoaswitch.com>
1 parent 4626dfe commit 74a1b22

File tree

5 files changed

+173
-24
lines changed

5 files changed

+173
-24
lines changed

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
# SwiftUI Charts
2-
1+
## SwiftUI Charts
32
Build custom charts with SwiftUI
43

54
<center>
@@ -60,10 +59,25 @@ Chart(data: [0.1, 0.3, 0.2, 0.5, 0.4, 0.9, 0.1])
6059
```swift
6160
Chart(data: matrix)
6261
.chartStyle(
63-
StackedColumnChartStyle(spacing: 2, colors: [.yellow, .orange, .red])
62+
StackedColumnChartStyle(spacing: 2, cornerRadius: 3, colors: [.yellow, .orange, .red])
6463
)
6564
```
6665

66+
## Install
67+
Add `Charts` to your project with Swift Package Manager
68+
69+
```swift
70+
// swift-tools-version:5.1
71+
import PackageDescription
72+
73+
let package = Package(
74+
name: "YOUR_PROJECT",
75+
dependencies: [
76+
.package(url: "https://github.com/spacenation/swiftui-charts.git", from: "1.0.0"),
77+
]
78+
)
79+
```
80+
6781
## Roadmap
6882
- Bar chart style
6983

Sources/Charts/Chart/Chart.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import SwiftUI
2-
import Shapes
2+
@_exported import Shapes
33

44
public struct Chart: View {
55
@Environment(\.chartStyle) private var style
@@ -33,6 +33,7 @@ struct Chart_Previews: PreviewProvider {
3333
LineChartDemo()
3434
AreaChartDemo()
3535
ColumnChartDemo()
36+
BarChartDemo()
3637
StackedAreaChartDemo()
3738
CompositeChartDemo()
3839
}
@@ -41,14 +42,14 @@ struct Chart_Previews: PreviewProvider {
4142

4243

4344
private struct LineChartDemo: View {
44-
@State var data1: [CGFloat] = (2010...2020).map { _ in .random(in: 0.1...1.0) }
45+
@State var data1: [CGFloat] = (2010...2020).map { _ in .random(in: 0.0...1.0) }
4546
@State var trim: CGFloat = 0
4647

4748
var body: some View {
4849
HStack {
4950
VStack {
50-
AxisLabels(.vertical, data: 1...10, id: \.self) {
51-
Text("\(110 - $0 * 10)")
51+
AxisLabels(.vertical, data: (-10...10).reversed(), id: \.self) {
52+
Text("\($0 * 10)")
5253
.fontWeight(.bold)
5354
.font(Font.system(size: 8))
5455
.foregroundColor(.secondary)
@@ -66,7 +67,7 @@ private struct LineChartDemo: View {
6667
)
6768
.padding()
6869
.background(
69-
GridPattern(horizontalLines: 10 + 1, verticalLines: data1.count + 1)
70+
GridPattern(horizontalLines: 20 + 1, verticalLines: data1.count + 1)
7071
.inset(by: 1)
7172
.stroke(Color.gray.opacity(0.1), style: .init(lineWidth: 2, lineCap: .round))
7273
)
@@ -115,7 +116,7 @@ private struct AreaChartDemo: View {
115116
}
116117

117118
private struct ColumnChartDemo: View {
118-
@State var data3: [CGFloat] = (0..<10).map { _ in .random(in: 0.1...1.0) }
119+
@State var data3: [CGFloat] = [-0.5,-0.2,-0.1,0.1,0.2,0.5,1]
119120

120121
var body: some View {
121122
Chart(data: data3)
@@ -129,6 +130,21 @@ private struct ColumnChartDemo: View {
129130
}
130131
}
131132

133+
private struct BarChartDemo: View {
134+
@State var data3: [CGFloat] = (0..<10).map { _ in .random(in: 0.1...1.0) }
135+
136+
var body: some View {
137+
Chart(data: data3)
138+
.chartStyle(
139+
BarChartStyle(bar: Capsule().foregroundColor(.green), spacing: 2)
140+
)
141+
.padding()
142+
.background(Color.gray.opacity(0.1))
143+
.cornerRadius(16)
144+
.padding()
145+
}
146+
}
147+
132148
private struct StackedAreaChartDemo: View {
133149
@State var matrixData: [[CGFloat]] = (0..<20).map { _ in (0..<3).map { _ in CGFloat.random(in: 0.00...0.33) } }
134150

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
import Shapes
3+
import SwiftUI
4+
5+
public struct BarChartStyle<Bar: View>: ChartStyle {
6+
7+
private let bar: Bar
8+
private let spacing: CGFloat
9+
10+
public init(bar: Bar, spacing: CGFloat = 0) {
11+
self.bar = bar
12+
self.spacing = spacing
13+
}
14+
15+
16+
public func makeBody(configuration: Configuration) -> some View {
17+
let data: [ColumnData] = configuration.dataMatrix
18+
.map { $0.reduce(0, +) }
19+
.enumerated()
20+
.map { ColumnData(id: $0.offset, data: $0.element) }
21+
22+
return GeometryReader { geometry in
23+
self.barChart(in: geometry, data: data)
24+
}
25+
}
26+
27+
func barChart(in geometry: GeometryProxy, data: [ColumnData]) -> some View {
28+
let barHeight = (geometry.size.height - (CGFloat(data.count - 1) * spacing)) / CGFloat(data.count)
29+
30+
return ZStack(alignment: .topLeading) {
31+
ForEach(data) { element in
32+
self.bar
33+
.alignmentGuide(.top, computeValue: { dimension in
34+
CGFloat(element.id) * (spacing + barHeight)
35+
})
36+
.frame(width: element.data * geometry.size.width, height: barHeight)
37+
}
38+
}
39+
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .bottom)
40+
}
41+
42+
}

Sources/Charts/Chart/Styles/Column/ColumnChartStyle.swift

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,76 @@ public struct ColumnChartStyle<Column: View>: ChartStyle {
1111
.enumerated()
1212
.map { ColumnData(id: $0.offset, data: $0.element) }
1313

14+
let hasNegativeValues = data.contains(where: {$0.data < 0})
15+
1416
return GeometryReader { geometry in
15-
self.columnChart(in: geometry, data: data)
17+
self.columnChart(in: geometry, data: data, hasNegativeValues: hasNegativeValues)
1618
}
1719
}
1820

19-
private func columnChart(in geometry: GeometryProxy, data: [ColumnData]) -> some View {
21+
private func columnChart(in geometry: GeometryProxy, data: [ColumnData], hasNegativeValues: Bool) -> some View {
2022
let columnWidth = (geometry.size.width - (CGFloat(data.count - 1) * spacing)) / CGFloat(data.count)
2123

22-
return ZStack(alignment: .bottomLeading) {
23-
ForEach(data) { element in
24-
self.column
25-
.alignmentGuide(.leading, computeValue: { _ in self.leadingAlignmentGuide(for: element.id, in: geometry.size.width, dataCount: data.count) })
26-
.alignmentGuide(.bottom, computeValue: { _ in self.columnHeight(data: element.data, in: geometry.size.height) })
27-
.frame(width: columnWidth, height: self.columnHeight(data: element.data, in: geometry.size.height))
24+
return ZStack(alignment: .center) {
25+
26+
HStack(alignment: hasNegativeValues ? .center : .bottom, spacing: spacing) {
27+
ForEach(data) { element in
28+
self.column
29+
.frame(width: columnWidth, height: self.columnHeight(data: element.data, in: geometry.size.height, hasNegativeValues: hasNegativeValues))
30+
.offset(self.offset(for: element, geometry: geometry, columnWidth: columnWidth, dataCount: data.count, hasNegativeValues: hasNegativeValues))
31+
}
2832
}
33+
2934
}
30-
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .bottom)
35+
.frame(width: geometry.size.width, height: geometry.size.height, alignment: hasNegativeValues ? .center : .bottom)
36+
.background(ZStack {
37+
if hasNegativeValues {
38+
Path { path in
39+
path.addLine(from: CGPoint(x: 0, y:geometry.size.height/2), to: CGPoint(x: geometry.size.width, y: geometry.size.height/2))
40+
}
41+
.stroke(Color.gray.opacity(0.8), style: .init(lineWidth: 1, lineCap: .round))
42+
}
43+
})
3144
}
3245

33-
private func columnHeight(data: CGFloat, in availableHeight: CGFloat) -> CGFloat {
34-
availableHeight * data
46+
private func offset(for element: ColumnData, geometry: GeometryProxy, columnWidth: CGFloat, dataCount: Int, hasNegativeValues: Bool) -> CGSize {
47+
let x: CGFloat = 0
48+
var y: CGFloat = 0
49+
let height = self.columnHeight(data: element.data, in: geometry.size.height, hasNegativeValues: hasNegativeValues)
50+
51+
if hasNegativeValues {
52+
if element.data > 0 {
53+
y = -height/2
54+
}else {
55+
y = height/2
56+
}
57+
}
58+
59+
return CGSize(width: x, height: y)
60+
}
61+
62+
private func columnHeight(data: CGFloat, in availableHeight: CGFloat, hasNegativeValues: Bool) -> CGFloat {
63+
let height = availableHeight * abs(data)
64+
if hasNegativeValues {
65+
return height/2
66+
}
67+
return height
68+
}
69+
70+
private func bottomAlignmentGuide(dimension: ViewDimensions, for data:CGFloat, in availableHeight: CGFloat, hasNegativeValues: Bool) -> CGFloat {
71+
let height = self.columnHeight(data: data, in: availableHeight, hasNegativeValues: hasNegativeValues)
72+
if data < 0 {
73+
return availableHeight/2 - height
74+
}else if hasNegativeValues {
75+
return availableHeight/2 + height
76+
}
77+
78+
return height
3579
}
3680

3781
private func leadingAlignmentGuide(for index: Int, in availableWidth: CGFloat, dataCount: Int) -> CGFloat {
3882
let columnWidth = (availableWidth - (CGFloat(dataCount - 1) * spacing)) / CGFloat(dataCount)
39-
return (CGFloat(index) * columnWidth) + (CGFloat(index - 1) * spacing)
83+
return availableWidth - (CGFloat(index) * columnWidth) + (CGFloat(index - 1) * spacing)
4084
}
4185

4286
public init(column: Column, spacing: CGFloat = 8) {
@@ -61,3 +105,33 @@ public extension ColumnChartStyle where Column == DefaultColumnView {
61105
self.init(column: DefaultColumnView(), spacing: spacing)
62106
}
63107
}
108+
109+
110+
struct ColumnChartPreview: PreviewProvider {
111+
112+
@State static var data3: [CGFloat] = [-0.5,-0.2,-0.1,0.1,0.2,0.5,1]
113+
@State static var data4: [CGFloat] = [0.1,0.2,0.5,0.9]
114+
115+
static var previews: some View {
116+
Group {
117+
Chart(data: data3)
118+
.chartStyle(
119+
ColumnChartStyle(column: Capsule().foregroundColor(.green), spacing: 2)
120+
)
121+
.padding()
122+
.background(Color.gray.opacity(0.1))
123+
.cornerRadius(16)
124+
.padding()
125+
126+
Chart(data: data4)
127+
.chartStyle(
128+
ColumnChartStyle(column: Capsule().foregroundColor(.green), spacing: 2)
129+
)
130+
.padding()
131+
.background(Color.gray.opacity(0.1))
132+
.cornerRadius(16)
133+
.padding()
134+
}
135+
136+
}
137+
}

Sources/Charts/Chart/Styles/Column/StackedColumnChartStyle.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Shapes
44
public struct StackedColumnChartStyle<Column: View>: ChartStyle {
55
private let spacing: CGFloat
66
private let column: ([CGFloat]) -> Column
7+
private let cornerRadius: CGFloat
78

89
public func makeBody(configuration: Self.Configuration) -> some View {
910
GeometryReader { geometry in
@@ -20,6 +21,7 @@ public struct StackedColumnChartStyle<Column: View>: ChartStyle {
2021
.alignmentGuide(.leading, computeValue: { _ in self.leadingAlignmentGuide(for: enumeratedData.offset, in: geometry.size.width, dataCount: data.count) })
2122
.alignmentGuide(.bottom, computeValue: { _ in self.columnHeight(data: enumeratedData.element, in: geometry.size.height) })
2223
.frame(width: columnWidth, height: self.columnHeight(data: enumeratedData.element, in: geometry.size.height))
24+
.cornerRadius(cornerRadius)
2325
}
2426
}
2527
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .bottom)
@@ -34,9 +36,10 @@ public struct StackedColumnChartStyle<Column: View>: ChartStyle {
3436
return (CGFloat(index) * columnWidth) + (CGFloat(index - 1) * spacing)
3537
}
3638

37-
init(spacing: CGFloat = 8, column: @escaping ([CGFloat]) -> Column) {
39+
init(spacing: CGFloat, cornerRadius: CGFloat, column: @escaping ([CGFloat]) -> Column) {
3840
self.spacing = spacing
3941
self.column = column
42+
self.cornerRadius = cornerRadius
4043
}
4144
}
4245

@@ -69,7 +72,7 @@ public struct DefaultStackedColumnView: View {
6972
}
7073

7174
public extension StackedColumnChartStyle where Column == DefaultStackedColumnView {
72-
init(spacing: CGFloat = 8, colors: [Color] = [.red, .orange, .yellow, .green, .blue, .purple]) {
73-
self.init(spacing: spacing, column: { DefaultStackedColumnView(data: $0, colors: colors) })
75+
init(spacing: CGFloat = 8, cornerRadius: CGFloat = 0, colors: [Color] = [.red, .orange, .yellow, .green, .blue, .purple]) {
76+
self.init(spacing: spacing, cornerRadius: cornerRadius, column: { DefaultStackedColumnView(data: $0, colors: colors) })
7477
}
7578
}

0 commit comments

Comments
 (0)