Created
August 24, 2021 03:59
-
-
Save calleric/a9a8ffbfa39fe6e1a7cd134124be6b7f to your computer and use it in GitHub Desktop.
Code to create a Bar Chart in SwiftUI with multiple datasets
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// gist file for Bar Chart with multiple data sets in SwiftUI | |
// https://swdevnotes.com/swift/2021/bar-chart-with-multiple-datasets-in-swiftui/ | |
// | |
// All code in one ContentView.swift file just for sharing | |
// | |
// Created by Eric on 22/08/2021. | |
// | |
import SwiftUI | |
struct DataItem: Identifiable { | |
let name: String | |
let values: [Double] | |
let id = UUID() | |
} | |
struct BarChartData { | |
let keys: [String] | |
let data: [DataItem] | |
} | |
// Use colors from MatPlotLib ListedColormap (matplotlib.pyplot.cm.tab10) | |
fileprivate var colors:[Color] = | |
[ | |
Color(red: 0.122, green: 0.467, blue: 0.706), // #1f77b4 | |
Color(red: 1.000, green: 0.498, blue: 0.055), // #ff7f0e | |
Color(red: 0.173, green: 0.627, blue: 0.173), // #2ca02c | |
Color(red: 0.839, green: 0.153, blue: 0.157), // #d62728 | |
Color(red: 0.580, green: 0.404, blue: 0.741), // #9467bd | |
Color(red: 0.549, green: 0.337, blue: 0.294), // #8c564b | |
Color(red: 0.890, green: 0.467, blue: 0.761), // #e377c2 | |
Color(red: 0.498, green: 0.498, blue: 0.498), // #7f7f7f | |
Color(red: 0.737, green: 0.741, blue: 0.133), // #bcbd22 | |
Color(red: 0.090, green: 0.745, blue: 0.812) // #17becf | |
] | |
struct ChartColors { | |
static func BarColor(_ colorIndex :Int) -> Color { | |
colors[colorIndex % colors.count] | |
} | |
} | |
struct ContentView: View { | |
@State private var isShowingYAxis = true | |
@State private var isShowingXAxis = true | |
@State private var isShowingHeading = true | |
@State private var isShowingKey = true | |
let chartData = BarChartData( | |
keys: ["Previous", "Current"], | |
data: [ | |
DataItem(name: "Mon", values: [890, 800]), | |
DataItem(name: "Tue", values: [657, 900]), | |
DataItem(name: "Wed", values: [1282, 1650]), | |
DataItem(name: "Thu", values: [285, 637]), | |
DataItem(name: "Fri", values: [915, 582]), | |
DataItem(name: "Sat", values: [487, 480]), | |
DataItem(name: "Sun", values: [731, 1173]) | |
]) | |
var body: some View { | |
VStack { | |
VStack { | |
Spacer() | |
.frame(height:20) | |
BarChartView( | |
title: "Daily step count", | |
chartData: chartData, | |
isShowingYAxis: isShowingYAxis, | |
isShowingXAxis: isShowingXAxis, | |
isShowingHeading: isShowingHeading, | |
isShowingKey: isShowingKey) | |
.animation(.default) | |
.frame(width: 400, height: 450, alignment: .center) | |
Spacer() | |
.frame(height:50) | |
VStack { | |
Text("Chart Settings") | |
.font(.title2) | |
Toggle("Show Y axis", isOn: $isShowingYAxis) | |
Toggle("Show X axis", isOn: $isShowingXAxis) | |
Toggle("Show heading", isOn: $isShowingHeading) | |
Toggle("Show Key", isOn: $isShowingKey) | |
} | |
.padding(.horizontal, 50) | |
Spacer() | |
} | |
} | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var previews: some View { | |
ContentView() | |
} | |
} | |
struct AxisParameters { | |
static func getTicks(top:Int) -> [Int] { | |
var step = 0 | |
var high = top | |
switch(top) { | |
case 0...8: | |
step = 1 | |
case 9...17: | |
step = 2 | |
case 18...50: | |
step = 5 | |
case 51...170: | |
step = 10 | |
case 171...500: | |
step = 50 | |
case 501...1700: | |
step = 200 | |
case 1701...5000: | |
step = 500 | |
case 5001...17000: | |
step = 1000 | |
case 17001...50000: | |
step = 5000 | |
case 50001...170000: | |
step = 10000 | |
case 170001...1000000: | |
step = 10000 | |
default: | |
step = 10000 | |
} | |
high = ((top/step) * step) + step + step | |
var ticks:[Int] = [] | |
for i in stride(from: 0, to: high, by: step) { | |
ticks.append(i) | |
} | |
return ticks | |
} | |
} | |
struct BarChartView: View { | |
var title: String | |
var chartData: BarChartData | |
var isShowingYAxis = true | |
var isShowingXAxis = true | |
var isShowingHeading = true | |
var isShowingKey = true | |
var body: some View { | |
let data = chartData.data | |
GeometryReader { gr in | |
let axisWidth = gr.size.width * (isShowingYAxis ? 0.15 : 0.0) | |
let axisHeight = gr.size.height * (isShowingXAxis ? 0.1 : 0.0) | |
let keyHeight = gr.size.height * (isShowingKey ? 0.1 : 0.0) | |
let headHeight = gr.size.height * (isShowingHeading ? 0.14 : 0.0) | |
let fullChartHeight = gr.size.height - axisHeight - headHeight - keyHeight | |
let maxValue = data.flatMap { $0.values }.max()! | |
let tickMarks = AxisParameters.getTicks(top: Int(maxValue)) | |
let maxTickHeight = fullChartHeight * 0.95 | |
let scaleFactor = maxTickHeight / CGFloat(tickMarks[tickMarks.count-1]) | |
VStack(spacing:0) { | |
if isShowingHeading { | |
ChartHeaderView(title: title) | |
.frame(height: headHeight) | |
} | |
ZStack { | |
Rectangle() | |
.fill(Color(#colorLiteral(red: 0.8906477705, green: 0.9005050659, blue: 0.8208766097, alpha: 1))) | |
VStack(spacing:0) { | |
if isShowingKey { | |
KeyView(keys: chartData.keys) | |
.frame(height: keyHeight) | |
} | |
HStack(spacing:0) { | |
if isShowingYAxis { | |
YaxisView(ticks: tickMarks, scaleFactor: Double(scaleFactor)) | |
.frame(width:axisWidth, height: fullChartHeight) | |
} | |
ChartAreaView(data: data, scaleFactor: Double(scaleFactor)) | |
.frame(height: fullChartHeight) | |
} | |
HStack(spacing:0) { | |
Rectangle() | |
.fill(Color.clear) | |
.frame(width:axisWidth, height:axisHeight) | |
if isShowingXAxis { | |
XaxisView(data: data) | |
.frame(height:axisHeight) | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
struct ChartHeaderView: View { | |
var title: String | |
var body: some View { | |
VStack { | |
Text(title) | |
.font(.title3) | |
.fontWeight(.bold) | |
.multilineTextAlignment(.center) | |
} | |
} | |
} | |
struct ChartAreaView: View { | |
var data: [DataItem] | |
var scaleFactor: Double | |
var body: some View { | |
ZStack { | |
RoundedRectangle(cornerRadius: 5.0) | |
.fill(Color(#colorLiteral(red: 0.8906477705, green: 0.9005050659, blue: 0.8208766097, alpha: 1))) | |
VStack { | |
HStack(spacing:0) { | |
ForEach(data) { item in | |
BarView( | |
name: item.name, | |
values: item.values, | |
scaleFactor: scaleFactor) | |
} | |
} | |
} | |
} | |
} | |
} | |
struct BarView: View { | |
var name: String | |
var values: [Double] | |
var scaleFactor: Double | |
var body: some View { | |
GeometryReader { gr in | |
let textWidth = gr.size.width * 0.80 | |
let padWidth = gr.size.width * 0.07 | |
HStack(spacing:0) { | |
Spacer() | |
.frame(width:padWidth) | |
ForEach(values.indices) { i in | |
let barHeight = values[i] * scaleFactor | |
ZStack { | |
VStack(spacing:0) { | |
Spacer() | |
Rectangle() | |
.fill(ChartColors.BarColor(i)) | |
.frame(height: min(5.0, CGFloat(barHeight)), alignment: .trailing) | |
} | |
VStack(spacing:0) { | |
Spacer() | |
RoundedRectangle(cornerRadius:5.0) | |
.fill(ChartColors.BarColor(i)) | |
.frame(height: CGFloat(barHeight), alignment: .trailing) | |
.overlay( | |
Text("\(values[i], specifier: "%.0F")") | |
.font(.footnote) | |
.foregroundColor(.white) | |
.fontWeight(.bold) | |
.frame(width: textWidth) | |
.rotationEffect(Angle(degrees: -90)) | |
.offset(y:15) | |
, | |
alignment: .top | |
) | |
} | |
} | |
} | |
Spacer() | |
.frame(width:padWidth) | |
} | |
} | |
} | |
} | |
struct YaxisView: View { | |
var ticks: [Int] | |
var scaleFactor: Double | |
var body: some View { | |
GeometryReader { gr in | |
let fullChartHeight = gr.size.height | |
ZStack { | |
// y-axis line | |
Rectangle() | |
.fill(Color.black) | |
.frame(width:1.5) | |
.offset(x: (gr.size.width/2.0)-1, y: 1) | |
// Tick marks | |
ForEach(ticks, id:\.self) { t in | |
HStack { | |
Spacer() | |
Text("\(t)") | |
.font(.footnote) | |
Rectangle() | |
.frame(width: 10, height: 1) | |
} | |
.offset(y: (fullChartHeight/2.0) - (CGFloat(t) * CGFloat(scaleFactor))) | |
} | |
} | |
} | |
} | |
} | |
struct XaxisView: View { | |
var data: [DataItem] | |
var body: some View { | |
GeometryReader { gr in | |
let labelWidth = (gr.size.width * 0.9) / CGFloat(data.count) | |
let padWidth = (gr.size.width * 0.05) / CGFloat(data.count) | |
ZStack { | |
Rectangle() | |
.fill(Color(#colorLiteral(red: 0.8906477705, green: 0.9005050659, blue: 0.8208766097, alpha: 1))) | |
Rectangle() | |
.fill(Color.black) | |
.frame(height: 1.5) | |
.offset(x: 0, y: -(gr.size.height/2.0)) | |
HStack(spacing:0) { | |
ForEach(data) { item in | |
Text(item.name) | |
.font(.footnote) | |
.frame(width:labelWidth, height: gr.size.height) | |
} | |
.padding(.horizontal, padWidth) | |
} | |
} | |
} | |
} | |
} | |
struct KeyView: View { | |
let keys: [String] | |
var body: some View { | |
HStack { | |
ForEach(keys.indices) { i in | |
HStack(spacing:0) { | |
Image(systemName: "square.fill") | |
.foregroundColor(ChartColors.BarColor(i)) | |
Text("\(keys[i])") | |
} | |
.font(.footnote) | |
} | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment