As replacement for SwiftUI Charts, SwiftUIGraphs features a multi-line chart, stacked bar chart and pie chart with many customisable properties.
SwiftUIGraphs is a Swift package for iOS, iPadOS (14.0 and later) as well as MacOS (11.0 and later). It features a line chart, bar chart and pie chart for data visualization and has many customization options.
The folder SwiftUIGraphsExample contains an example project, make sure to check it out for more implementation details. All public structs / classes include in-code-documentation.
Installation through the Swift Package Manager (SPM) is recommended.
SPM: Select your project (not the target) and then select the Swift Packages tab. Clicking + and typing SwiftUIGraphs should find the package on github. Otherwise copy and paste the URL of this repo.
Check out the version history below for the current version.
Make sure to import SwiftUIGraphs in every file where you use SwiftUIGraphs.
import SwiftUIGraphs
Check out the following examples. This repo also contains an example project to illustrate how to implement all three chart types. Check the in-code documentation for more details.
DYLineChartView supports the following modifiers:
Pass in one or several DYLineViews into DYLineChartView's lineViews closure. You can attach the following modifiers to DYLineView:
struct MultiLineChartExample: View {
let colors: [Color] = [.blue, .orange, .green]
@State private var dataPointArrays: Array<[DYDataPoint]> = []
@State private var blueSelectedDataPoint: DYDataPoint?
@State private var orangeSelectedDataPoint: DYDataPoint?
@State private var greenSelectedDataPoint: DYDataPoint?
var body: some View {
GeometryReader { proxy in
VStack {
DYLineChartView(allDataPoints: Array(self.dataPointArrays.joined()), lineViews: { parentProps in
let selectedPoints = [$blueSelectedDataPoint, $orangeSelectedDataPoint, $greenSelectedDataPoint]
ForEach(0..<dataPointArrays.count, id:\.self) { i in
DYLineView(dataPoints: dataPointArrays[i], selectedDataPoint: selectedPoints[i], pointView: { _ in
self.pointViewFor(index: i)
}, selectorView: self.selectorPointViewFor(index: i), parentViewProperties: parentProps)
.lineStyle(color: colors[i])
.selectedPointIndicatorLineStyle(xLineColor: colors[i], yLineColor: colors[i])
}
})
.markerGridLine(coordinate: 0, color: .red)
.yAxisLabelFontSize(UIDevice.current.userInterfaceIdiom == .phone ? 8 : 10)
.yAxisLabelStringValue({ yValue in
self.stringified(value:yValue)
})
.xAxisLabelFontSize(UIDevice.current.userInterfaceIdiom == .phone ? 8 : 10)
.xAxisLabelStringValue({ xValue in
self.stringified(value: xValue)
})
.frame(height: self.chartHeight(proxy: proxy))
.padding()
self.legendView.padding()
Spacer()
}
}.navigationTitle("Some Random Data Sets")
.onAppear {
generateDataPoints()
}
}
func chartHeight(proxy: GeometryProxy)->CGFloat {
return proxy.size.height > proxy.size.width ? proxy.size.height * 0.4 : proxy.size.height * 0.75
}
var legendView: some View {
return HStack(spacing:15) {
let selectedPoints = [blueSelectedDataPoint, orangeSelectedDataPoint, greenSelectedDataPoint]
ForEach(0..<selectedPoints.count, id:\.self) { i in
if let dataPoint = selectedPoints[i] {
HStack {
self.pointViewFor(index: i)
Text("X: \(self.stringified(value: dataPoint.xValue))")
Text("Y: \(self.stringified(value: dataPoint.yValue))")
}.font(UIDevice.current.userInterfaceIdiom == .pad ? .body : .caption)
}
}
Spacer()
}
}
func stringified(value: Double)->String {
let formatter = NumberFormatter()
formatter.maximumFractionDigits = 1
return formatter.string(for: value)!
}
func generateDataPoints() {
var dataPointArrays: Array<[DYDataPoint]> = []
for _ in 0..<3 {
var dataPoints: [DYDataPoint] = []
var xValue = Double.random(in: 1...1.5)
for _ in 0..<12 {
let yValue = Double.random(in: -10...40)
let dataPoint = DYDataPoint(xValue: xValue, yValue: yValue)
dataPoints.append(dataPoint)
xValue += Double.random(in: 0.5...1)
}
dataPointArrays.append(dataPoints)
}
self.dataPointArrays = dataPointArrays
}
func pointViewFor(index: Int)-> some View {
Group {
switch index {
case 0:
DYLinePointView(borderColor: colors[index])
case 1:
DYLinePointView(shape: Rectangle(), borderColor: colors[index], edgeLength: 10)
default:
DYLinePointView(shape: Triangle(), borderColor: colors[index])
}
}
}
func selectorPointViewFor(index: Int)-> some View {
Group {
switch index {
case 0:
DYSelectorPointView()
case 1:
DYSelectorPointView(shape: Rectangle(), shapeSize: 12, shapeHaloSize: 24)
default:
DYSelectorPointView(shape: Triangle(), haloOffset: CGSize(width: 0, height: -14 / 6))
}
}
}
}
Check out the example project for details (included in the package).
In order to try out the stock price example, you need to sign up with https://iexcloud.io for free to get an authentication token.
Area Chart with different color per line segment example:
Additionally, you can add a drop shadow underneath the gradient (and / or the line):
DYBarChartView supports multiple data series from version 1.0. Each bar is represented by a DYBarDataSet - check out the example below for details.
DYBarChartView supports the following modifiers:
struct MultiBarChartExample: View {
let colors: [Color] = [.blue, .orange, .green]
let titles = ["Energy", "Pharmaceutical", "Agriculture"]
@State var barDataSets: [DYBarDataSet] = []
@State var selectedBarDataSet: DYBarDataSet?
var body: some View {
GeometryReader { proxy in
VStack {
DYBarChartView(barDataSets: barDataSets, selectedBarDataSet: $selectedBarDataSet)
.barDropShadow(Shadow(color: .gray, radius:8, x:-4, y:-3))
.selectedBar(borderColor: .purple, dropShadow: Shadow(color: .black.opacity(0.7), radius:10, x:-7, y:-5))
.yAxisLabelFontSize(UIDevice.current.userInterfaceIdiom == .phone ? 8 : 10)
.markerGridLine(yCoordinate: 0, color: .red)
.xAxisLabelFontSize(UIDevice.current.userInterfaceIdiom == .phone ? 8 : 10)
.frame(height:chartHeight(proxy: proxy))
if self.barDataSets.isEmpty == false {
HStack {
self.selectedDataSetDetailView()
Spacer()
}
}
Spacer()
}.padding()
}.navigationTitle("Profits & Losses Mio. USD per Division per Year")
.onAppear {
self.generateExampleData()
}
}
func chartHeight(proxy: GeometryProxy)->CGFloat {
return proxy.size.height > proxy.size.width ? proxy.size.height * 0.4 : proxy.size.height * 0.65
}
func selectedDataSetDetailView()->some View {
Group {
if let barDataSet = selectedBarDataSet {
VStack(alignment: .leading) {
Text(barDataSet.xAxisLabel).font(.callout).bold()
HStack {
if barDataSet.netValue >= 0 {
Text("Net Profit:")
} else {
Text("Net Loss:").foregroundColor(.red).bold()
}
Text(abs(barDataSet.netValue).toCurrencyString(maxDigits:1) + " million").foregroundColor(barDataSet.netValue >= 0 ? .primary : .red)
Spacer()
}
VStack(spacing: 0) {
ForEach(0..<barDataSet.fractions.count, id:\.self) { i in
let fraction = barDataSet.fractions[i]
HStack(spacing: 0) {
HStack {
Rectangle().fill(colors[i]).frame(width: 15, height: 15)
Text(fraction.title + ": ")
Spacer()
}
Text(fraction.value.toCurrencyString(maxDigits:1) + " million").foregroundColor(fraction.value >= 0 ? .primary : .red)
Spacer()
}.font(.callout)
}
}.transition(AnyTransition.opacity)
}.frame(maxWidth: 300).padding()
}
}
}
func generateExampleData() {
var barDataSets: [DYBarDataSet] = []
var currentYear = 2012
for _ in 0..<10 {
let firstValue = Double.random(in: -10 ..< 50)
let secondValue = Double.random(in: -20 ..< 40)
let thirdValue = Double.random(in: -30 ..< 30)
let values = [firstValue, secondValue, thirdValue]
let xValueLabel = "\(currentYear)"
var fractions: [DYBarDataFraction] = []
for i in 0..<values.count {
let fraction = DYBarDataFraction(value: values[i], title:titles[i], gradient: LinearGradient(colors: [colors[i]], startPoint: .top, endPoint: .bottom)) {
Text(values[i].toDecimalString(maxFractionDigits: 1)).font(.footnote).lineLimit(1).foregroundColor(.white).eraseToAnyView()
}
fractions.append(fraction)
}
let dataSet = DYBarDataSet(fractions: fractions, xAxisLabel: xValueLabel, labelView: { value in
let text = value != 0 ? value.toDecimalString(maxFractionDigits: 1) : ""
return Text(text).font(.footnote).eraseToAnyView()
})
barDataSets.append(dataSet)
currentYear += 1
}
self.barDataSets = barDataSets
}
}
Simple bar chart example:
DYPieChartView supports the following modifiers:
Set the innerCircleRadiusFraction(_ fraction: CGFloat = 0) modifier to a value larger than 0 and smaller than 1 to turn a pie chart into a ring chart. Details below.
struct RingChartAndDetailPieChartExample: View {
@State var detailChartSelectedSlice: DYPieFraction?
@State private var pieScale:CGSize = .zero
@StateObject var chartModel: ChartModel = ChartModel()
@Namespace var animationNamespace
var body: some View {
GeometryReader { proxy in
if proxy.size.height > proxy.size.width {
VStack {
self.contentView(isPortrait: true)
}
} else {
HStack {
self.contentView(isPortrait: false)
}
}
}.navigationTitle("Sales by Country")
}
func contentView(isPortrait: Bool)-> some View {
Group {
self.mainPieChart(isPortrait: isPortrait)
if detailChartVisibleCondition {
self.otherCategoryDetailsPieChart()
}
}
}
func mainPieChart(isPortrait: Bool)->some View {
VStack(spacing: 10) {
DYPieChartView(data: chartModel.data, selectedSlice: $chartModel.selectedSlice, sliceLabelView: { (fraction) in
self.sliceLabelContentView(fraction: fraction, data:self.chartModel.data, textColor: .white)
}, animationNamespace: animationNamespace)
.hideMultiFractionSliceOnSelection(true)
.innerCircleRadiusFraction(0.3)
.background(Circle().fill(Color(.systemBackground)).shadow(color: detailChartVisibleCondition ? .clear : .gray, radius:5))
.rotationEffect(detailChartVisibleCondition ? Angle(degrees: isPortrait ? 45 : -40) : Angle(degrees: 0))
}
.scaleEffect(self.pieScale)
.padding()
.onAppear {
withAnimation(.spring()) {
self.pieScale = CGSize(width: 1, height: 1)
}
}
}
func otherCategoryDetailsPieChart()->some View {
VStack(spacing: 5) {
DYPieChartView(data: chartModel.data[1].detailFractions, selectedSlice: $detailChartSelectedSlice, sliceLabelView: { (fraction) in
self.detailChartSliceLabelView(fraction: fraction, data: chartModel.data[1].detailFractions)
}, animationNamespace: animationNamespace)
.minimumFractionForSliceLabelOffset(0.11)
.background(Circle().fill(Color(.systemBackground)).shadow(radius: 10))
.padding(50)
.matchedGeometryEffect(id: self.chartModel.data[1].id, in: self.animationNamespace)
}
}
func detailChartSliceLabelView(fraction: DYPieFraction, data: [DYPieFraction])->some View {
Group {
if fraction.value / data.reduce(0, { $0 + $1.value}) >= 0.11 || self.detailChartSelectedSlice == fraction {
self.sliceLabelContentView(fraction: fraction, data:data, textColor: fraction.value / data.reduce(0, { $0 + $1.value}) >= 0.11 ? .white : .primary)
}
}
}
func sliceLabelContentView(fraction: DYPieFraction, data:[DYPieFraction], textColor: Color)-> some View {
VStack {
Text(fraction.title).font(sliceLabelViewFont).lineLimit(2).frame(maxWidth: 85)
Text(String(format:"%.0f units", fraction.value)).font(sliceLabelViewFont).bold()
Text(fraction.value.percentageString(totalValue: data.reduce(0) { $0 + $1.value})).font(sliceLabelViewFont)
}.foregroundColor(textColor)
}
var sliceLabelViewFont: Font {
return UIDevice.current.userInterfaceIdiom == .pad ? .callout : .caption
}
var detailChartVisibleCondition: Bool {
self.chartModel.selectedSlice == chartModel.data[1]
}
}
Fixes a critical bug in DYLineView.
Various minor updates:
Various updates for MacOS native support.
Minor update: Renaming selectorView parameter in DYLineView to selectorPointView.
You can now set a label font (UIFont) for the x- and y-axis tick labels (default is UIFont.systemFont(ofSize: 8)). The default color of the line in a line chart is now primary instead of orange. Bug fix: setting userInteraction to false in a DYLineView now works properly when the userInteraction property is set to true in the DYLineChartView modifier.
BREAKING changes:
Bug fix: in the line chart, user interaction (provided it is switched on) works again if showAppearAnimation set to false
Added showAppearAnimation parameter to settings of DYLineChartView and DYBarChartView. If it is set to false, the line, line gradient (if any) and the bars will appear instantly without any transition animation.
Bug fixes:
"Shady update": You can now set drop shadows to show underneath the line and line gradient and underneath each bar and pie chart slice. Moreover, in the DYGridChartHeaderView you can now set the selected y-value text label to a different text color. Bug fixes:
Added allowUserInteration parameter to all three chart type settings (default is true).
Added lineAnimationDuration property to DYLineChartSettings.
Added DYLineChartSettings property that allows switching the interpolation type for path drawing between points to linear instead of quadCurve.
It is now possible to set individual colors per data point and per line section using closures in the initialiser of DYLineChartView. Additionally, it is possible to set a different color per each bar in the DYBarChartView initialiser. Special thanks to SAleksiev for his suggestion and help.
Initial public release. Added documentation to all public struct and class initializers. Minor visual improvements.
SwiftUIGraphs is available under the MIT license. See the LICENSE file for more info.