Created
August 29, 2021 12:35
-
-
Save calleric/d212215edca3c3314a905eaabc74bcf9 to your computer and use it in GitHub Desktop.
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 Vertical and Horizontal Bar Charts in SwiftUI | |
// https://swdevnotes.com/swift/2021/horizontal-bar-chart-in-swiftui/ | |
// | |
// All code in one ContentView.swift file just for sharing | |
// | |
// Created by Eric on 29/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 = false | |
@State private var isShowingXAxis = true | |
@State private var isShowingHeading = true | |
@State private var isShowingKey = true | |
@State private var isHShowingYAxis = true | |
@State private var isHShowingXAxis = false | |
@State private var isHShowingHeading = true | |
@State private var isHShowingKey = true | |
let chartData = BarChartData( | |
keys: ["Male", "Female"], | |
data: [ | |
DataItem(name: "Somalia", values: [127, 115]), | |
DataItem(name: "Nigeria", values: [127, 113]), | |
DataItem(name: "Chad", values: [125, 112]), | |
DataItem(name: "Central African Republic", values: [123, 110]), | |
DataItem(name: "Sierra Leone", values: [111, 99]) | |
]) | |
var body: some View { | |
ScrollView() { | |
VStack { | |
VStack { | |
Spacer() | |
.frame(height:20) | |
BarChartView( | |
title: "Under 5 Mortality Rate by Gender [2018]", | |
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() | |
} | |
VStack { | |
Spacer() | |
.frame(height:20) | |
BarChartHView( | |
title: "Under 5 Mortality Rate by Gender [2018]", | |
chartData: chartData, | |
isShowingYAxis: isHShowingYAxis, | |
isShowingXAxis: isHShowingXAxis, | |
isShowingHeading: isHShowingHeading, | |
isShowingKey: isHShowingKey) | |
.animation(.default) | |
.frame(width: 400, height: 450, alignment: .center) | |
Spacer() | |
.frame(height:50) | |
VStack { | |
Text("Chart Settings") | |
.font(.title2) | |
Toggle("Show Y axis", isOn: $isHShowingYAxis) | |
Toggle("Show X axis", isOn: $isHShowingXAxis) | |
Toggle("Show heading", isOn: $isHShowingHeading) | |
Toggle("Show Key", isOn: $isHShowingKey) | |
} | |
.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 scaleFactor = (fullChartHeight * 0.95) / 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) | |
} | |
} | |
} | |
} | |
// Horizontal SwiftUI Views | |
struct BarChartHView: 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 fullChartWidth = gr.size.width - axisWidth | |
let maxValue = data.flatMap { $0.values }.max()! | |
let tickMarks = AxisParameters.getTicks(top: Int(maxValue)) | |
let scaleFactor = (fullChartWidth * 0.95) / 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 { | |
YaxisHView(data: data) | |
.frame(width:axisWidth, height: fullChartHeight) | |
} | |
ChartAreaHView(data: data, scaleFactor: Double(scaleFactor)) | |
.frame(height: fullChartHeight) | |
} | |
HStack(spacing:0) { | |
Rectangle() | |
.fill(Color.clear) | |
.frame(width:axisWidth, height:axisHeight) | |
if isShowingXAxis { | |
XaxisHView(ticks: tickMarks, scaleFactor: Double(scaleFactor)) | |
.frame(height:axisHeight) | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
struct ChartAreaHView: 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 { | |
VStack(spacing:0) { | |
ForEach(data) { item in | |
BarHView( | |
name: item.name, | |
values: item.values, | |
scaleFactor: scaleFactor) | |
} | |
} | |
} | |
} | |
} | |
} | |
struct BarHView: View { | |
var name: String | |
var values: [Double] | |
var scaleFactor: Double | |
var body: some View { | |
GeometryReader { gr in | |
let padHeight = gr.size.height * 0.07 | |
VStack(spacing:0) { | |
Spacer() | |
.frame(height:padHeight) | |
ForEach(values.indices) { i in | |
let barSize = values[i] * scaleFactor | |
ZStack { | |
HStack(spacing:0) { | |
Rectangle() | |
.fill(ChartColors.BarColor(i)) | |
.frame(width: min(5.0, CGFloat(barSize)), alignment: .trailing) | |
Spacer() | |
} | |
HStack(spacing:0) { | |
RoundedRectangle(cornerRadius:5.0) | |
.fill(ChartColors.BarColor(i)) | |
.frame(width: CGFloat(barSize), alignment: .trailing) | |
.overlay( | |
Text("\(values[i], specifier: "%.0F")") | |
.font(.footnote) | |
.foregroundColor(.white) | |
.fontWeight(.bold) | |
.offset(x:-10, y:0) | |
, | |
alignment: .trailing | |
) | |
Spacer() | |
} | |
} | |
} | |
Spacer() | |
.frame(height:padHeight) | |
} | |
} | |
} | |
} | |
struct XaxisHView: View { | |
var ticks: [Int] | |
var scaleFactor: Double | |
var body: some View { | |
GeometryReader { gr in | |
let fullChartWidth = gr.size.width | |
ZStack { | |
// x-axis line | |
Rectangle() | |
.fill(Color.black) | |
.frame(height: 1.5) | |
.offset(x: 0, y: -(gr.size.height/2.0)) | |
// Tick marks | |
ForEach(ticks, id:\.self) { t in | |
VStack { | |
Rectangle() | |
.frame(width: 1, height: 10) | |
Text("\(t)") | |
.font(.footnote) | |
.rotationEffect(Angle(degrees: -45)) | |
Spacer() | |
} | |
.offset(x: (CGFloat(t) * CGFloat(scaleFactor)) - (fullChartWidth/2.0) - 1) | |
} | |
} | |
} | |
} | |
} | |
struct YaxisHView: View { | |
var data: [DataItem] | |
var body: some View { | |
GeometryReader { gr in | |
let labelHeight = (gr.size.height * 0.9) / CGFloat(data.count) | |
let padHeight = gr.size.height * 0.05 / CGFloat(data.count) | |
ZStack { | |
Rectangle() | |
.fill(Color(#colorLiteral(red: 0.8906477705, green: 0.9005050659, blue: 0.8208766097, alpha: 1))) | |
// y-axis line | |
Rectangle() | |
.fill(Color.black) | |
.frame(width:1.5) | |
.offset(x: (gr.size.width/2.0)-1, y: 1) | |
VStack(spacing:0) { | |
ForEach(data) { item in | |
Text(item.name) | |
.font(.footnote) | |
.frame(height: labelHeight) | |
} | |
.padding(.vertical, padHeight) | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment