Skip to content

Instantly share code, notes, and snippets.

@calleric
Created August 29, 2021 12:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save calleric/d212215edca3c3314a905eaabc74bcf9 to your computer and use it in GitHub Desktop.
Save calleric/d212215edca3c3314a905eaabc74bcf9 to your computer and use it in GitHub Desktop.
// 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