charting code added to the package

This commit is contained in:
Majid Jabrayilov
2020-07-20 17:12:14 +04:00
parent 63fe4db62c
commit dd837c7aef
14 changed files with 569 additions and 12 deletions
+6
View File
@@ -5,6 +5,12 @@ import PackageDescription
let package = Package(
name: "SwiftUICharts",
platforms: [
.macOS("10.16"),
.iOS("14"),
.watchOS("7"),
.tvOS("14")
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
+37
View File
@@ -0,0 +1,37 @@
//
// AxisView.swift
// CardioBot
//
// Created by Majid Jabrayilov on 6/27/20.
// Copyright © 2020 Majid Jabrayilov. All rights reserved.
//
import SwiftUI
struct AxisView: View {
let bars: [Bar]
var body: some View {
VStack {
bars.max().map {
Text(String(Int($0.value)))
.foregroundColor(.secondary)
.font(.caption)
}
Spacer()
bars.max().map {
Text(String(Int($0.value / 2)))
.foregroundColor(.secondary)
.font(.caption)
}
Spacer()
}
}
}
#if DEBUG
struct AxisView_Previews: PreviewProvider {
static var previews: some View {
AxisView(bars: Bar.mock)
}
}
#endif
+98
View File
@@ -0,0 +1,98 @@
//
// Bar.swift
// CardioBot
//
// Created by Majid Jabrayilov on 5/13/20.
// Copyright © 2020 Majid Jabrayilov. All rights reserved.
//
import SwiftUI
public struct Legend {
let color: Color
let label: LocalizedStringKey
let order: Int
public init(color: Color, label: LocalizedStringKey, order: Int = 0) {
self.color = color
self.label = label
self.order = order
}
}
extension Legend: Comparable {
public static func < (lhs: Self, rhs: Self) -> Bool {
lhs.order < rhs.order
}
}
extension Legend: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(color)
}
}
public struct Bar {
let value: Double
let label: LocalizedStringKey
let legend: Legend
let visible: Bool
public init(value: Double, label: LocalizedStringKey, legend: Legend, visible: Bool = true) {
self.value = value
self.label = label
self.legend = legend
self.visible = visible
}
}
extension Bar: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(legend)
hasher.combine(value)
}
}
extension Bar: Comparable {
public static func < (lhs: Bar, rhs: Bar) -> Bool {
lhs.value < rhs.value
}
}
#if DEBUG
extension Bar {
static var mock: [Bar] {
let highIntensity = Legend(color: .orange, label: "High Intensity", order: 5)
let buildFitness = Legend(color: .yellow, label: "Build Fitness", order: 4)
let fatBurning = Legend(color: .green, label: "Fat Burning", order: 3)
let warmUp = Legend(color: .blue, label: "Warm Up", order: 2)
let low = Legend(color: .gray, label: "Low", order: 1)
return [
.init(value: 70, label: "1", legend: low),
.init(value: 90, label: "2", legend: warmUp),
.init(value: 91, label: "3", legend: warmUp),
.init(value: 92, label: "4", legend: warmUp),
.init(value: 130, label: "5", legend: fatBurning),
.init(value: 124, label: "6", legend: fatBurning),
.init(value: 135, label: "7", legend: fatBurning),
.init(value: 133, label: "8", legend: fatBurning),
.init(value: 136, label: "9", legend: fatBurning),
.init(value: 138, label: "10", legend: fatBurning),
.init(value: 150, label: "11", legend: buildFitness),
.init(value: 151, label: "12", legend: buildFitness),
.init(value: 150, label: "13", legend: buildFitness),
.init(value: 136, label: "14", legend: fatBurning),
.init(value: 135, label: "15", legend: fatBurning),
.init(value: 130, label: "16", legend: fatBurning),
.init(value: 130, label: "17", legend: fatBurning),
.init(value: 150, label: "18", legend: buildFitness),
.init(value: 151, label: "19", legend: buildFitness),
.init(value: 150, label: "20", legend: buildFitness),
.init(value: 160, label: "21", legend: highIntensity),
.init(value: 159, label: "22", legend: highIntensity),
.init(value: 161, label: "23", legend: highIntensity),
.init(value: 158, label: "24", legend: highIntensity),
]
}
}
#endif
+67
View File
@@ -0,0 +1,67 @@
//
// BarChartView.swift
// SleepBot
//
// Created by Majid Jabrayilov on 6/21/19.
// Copyright © 2019 Majid Jabrayilov. All rights reserved.
//
import SwiftUI
public struct BarChartView: View {
let bars: [Bar]
var limit: Bar?
var showAxis = true
var showLabels = true
var labelCount = 3
var showLegends = true
public init(
bars: [Bar],
limit: Bar? = nil,
showAxis: Bool = true,
showLabels: Bool = true,
labelCount: Int = 3,
showLegends: Bool = true
) {
self.bars = bars
self.limit = limit
self.showAxis = showAxis
self.showLabels = showLabels
self.labelCount = labelCount
self.showLegends = showLegends
}
public var body: some View {
VStack {
HStack(spacing: 0) {
BarsView(bars: bars, limit: limit)
if showAxis {
AxisView(bars: bars)
.fixedSize(horizontal: true, vertical: false)
}
}
#if os(iOS)
if showLabels {
LabelsView(bars: bars, labelCount: labelCount)
.accessibility(hidden: true)
}
#endif
if showLegends {
LegendView(bars: limit.map { [$0] + bars} ?? bars)
.padding()
.accessibility(hidden: true)
}
}
}
}
#if DEBUG
struct BarChartView_Previews : PreviewProvider {
static var previews: some View {
let limit = Legend(color: .purple, label: "Trend")
let limitBar = Bar(value: 100, label: "Trend", legend: limit)
return BarChartView(bars: Bar.mock, limit: limitBar)
}
}
#endif
+76
View File
@@ -0,0 +1,76 @@
//
// BarsView.swift
// CardioBot
//
// Created by Majid Jabrayilov on 6/28/20.
// Copyright © 2020 Majid Jabrayilov. All rights reserved.
//
import SwiftUI
struct BarsView: View {
let bars: [Bar]
let limit: Bar?
private var max: Double {
if let max = bars.map({ $0.value }).max(), max > 0 {
return max
} else {
return 1
}
}
private var grid: some View {
ChartGrid(bars: bars)
.stroke(
Color.secondary,
style: StrokeStyle(
lineWidth: 1,
lineCap: .round,
lineJoin: .round,
miterLimit: 0,
dash: [1, 8],
dashPhase: 1
)
)
}
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .bottomTrailing) {
grid
HStack(alignment: .bottom, spacing: bars.count > 30 ? 0 : 2) {
ForEach(bars.filter(\.visible), id: \.self) { bar in
Capsule()
.fill(bar.legend.color)
.accessibility(label: Text(bar.label))
.accessibility(value: Text(bar.legend.label))
.frame(height: CGFloat(bar.value / self.max) * geometry.size.height)
}
}
limit.map { limit in
ZStack {
RoundedRectangle(cornerRadius: 4, style: .continuous)
.frame(height: 4)
.foregroundColor(limit.legend.color)
Text(limit.label)
.padding(.horizontal)
.foregroundColor(.white)
.background(limit.legend.color)
.clipShape(RoundedRectangle(cornerRadius: 4, style: .continuous))
}
.alignmentGuide(VerticalAlignment.bottom) { $0[.bottom] - $0.height / 2 }
.offset(y: CGFloat(limit.value / self.max) * -geometry.size.height)
}
}
}.frame(minHeight: 100)
}
}
#if DEBUG
struct BarsView_Previews: PreviewProvider {
static var previews: some View {
BarsView(bars: Bar.mock, limit: nil)
}
}
#endif
+44
View File
@@ -0,0 +1,44 @@
//
// ChartGrid.swift
// CardioBot
//
// Created by Majid Jabrayilov on 7/4/20.
// Copyright © 2020 Majid Jabrayilov. All rights reserved.
//
import SwiftUI
struct ChartGrid: Shape {
let bars: [Bar]
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: 0))
path.move(to: CGPoint(x: 0, y: rect.height))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.move(to: CGPoint(x: 0, y: rect.height / 2))
path.addLine(to: CGPoint(x: rect.width, y: rect.height / 2))
}
}
}
#if DEBUG
struct BarChartGrid_Previews: PreviewProvider {
static var previews: some View {
ChartGrid(bars: Bar.mock)
.stroke(
style: StrokeStyle(
lineWidth: 1,
lineCap: .round,
lineJoin: .round,
miterLimit: 0,
dash: [1, 8],
dashPhase: 1
)
)
}
}
#endif
@@ -0,0 +1,59 @@
//
// HorizontalBarChart.swift
// CardioBot
//
// Created by Majid Jabrayilov on 5/12/20.
// Copyright © 2020 Majid Jabrayilov. All rights reserved.
//
import SwiftUI
public struct HorizontalBarChart: View {
let bars: [Bar]
public init(bars: [Bar]) {
self.bars = bars
}
var max: Double { bars.max()?.value ?? 0}
public var body: some View {
VStack(alignment: .leading, spacing: 8) {
ForEach(bars, id: \.self) { bar in
HStack {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.foregroundColor(bar.legend.color)
.frame(width: CGFloat(bar.value / self.max) * 100, height: 16)
Circle()
.foregroundColor(bar.legend.color)
.frame(width: 8, height: 8)
Text(bar.legend.label) + Text(", ") + Text(bar.label)
}
}
}
}
}
struct HorizontalBarChart_Previews: PreviewProvider {
static var previews: some View {
let veryLow = Legend(color: .black, label: "Very Low")
let low = Legend(color: .gray, label: "Low")
let resting = Legend(color: .blue, label: "Resting")
let highResting = Legend(color: .orange, label: "High Resting")
let elevated = Legend(color: .red, label: "Elevated")
let bars: [Bar] = [
Bar(value: 0.1, label: "10%", legend: veryLow),
Bar(value: 0.15, label: "15%", legend: low),
Bar(value: 0.60, label: "60%", legend: resting),
Bar(value: 0.1, label: "10%", legend: highResting),
Bar(value: 0.05, label: "5%", legend: elevated)
]
return List {
return HorizontalBarChart(bars: bars)
}
.listStyle(InsetGroupedListStyle())
}
}
+45
View File
@@ -0,0 +1,45 @@
//
// LabelsView.swift
// CardioBot
//
// Created by Majid Jabrayilov on 6/27/20.
// Copyright © 2020 Majid Jabrayilov. All rights reserved.
//
import SwiftUI
struct LabelsView: View {
let bars: [Bar]
var labelCount = 3
private var threshold: Int {
let threshold = bars.count / labelCount
switch threshold {
case 0...1: return 1
case 1...2: return 2
default: return threshold
}
}
var body: some View {
HStack(spacing: 0) {
ForEach(bars.indexed(), id: \.1.self) { index, bar in
if index % self.threshold == 0 {
Text(bar.label)
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
.font(.caption)
Spacer()
}
}
}
}
}
#if DEBUG
struct LabelsView_Previews: PreviewProvider {
static var previews: some View {
LabelsView(bars: Bar.mock)
}
}
#endif
+38
View File
@@ -0,0 +1,38 @@
//
// LegendView.swift
// CardioBot
//
// Created by Majid Jabrayilov on 6/27/20.
// Copyright © 2020 Majid Jabrayilov. All rights reserved.
//
import SwiftUI
struct LegendView: View {
private let legends: [Legend]
init(bars: [Bar]) {
legends = Array(Set(bars.map { $0.legend })).sorted()
}
var body: some View {
LazyVGrid(columns: [.init(.adaptive(minimum: 100))], alignment: .leading) {
ForEach(legends, id: \.color) { legend in
HStack(alignment: .center) {
Circle()
.fill(legend.color)
.frame(width: 16, height: 16)
Text(legend.label)
}
}
}
}
}
#if DEBUG
struct LegendView_Previews: PreviewProvider {
static var previews: some View {
LegendView(bars: Bar.mock)
}
}
#endif
+85
View File
@@ -0,0 +1,85 @@
//
// LineChartView.swift
// CardioBot
//
// Created by Majid Jabrayilov on 6/27/20.
// Copyright © 2020 Majid Jabrayilov. All rights reserved.
//
import SwiftUI
public struct LineChartView: View {
let bars: [Bar]
public init(bars: [Bar]) {
self.bars = bars
}
private var gradient: LinearGradient {
let colors = bars.map(\.legend).map(\.color)
return LinearGradient(
gradient: Gradient(colors: colors),
startPoint: .leading,
endPoint: .trailing
)
}
public var body: some View {
VStack {
HStack(spacing: 0) {
ZStack {
ChartGrid(bars: bars)
.stroke(
Color.secondary,
style: StrokeStyle(
lineWidth: 1,
lineCap: .round,
lineJoin: .round,
miterLimit: 0,
dash: [1, 8],
dashPhase: 1
)
)
LineChart(bars: bars)
.fill(gradient)
.frame(minHeight: 100)
}
AxisView(bars: bars)
}
LabelsView(bars: bars)
LegendView(bars: bars)
}
}
}
private struct LineChart: Shape {
let bars: [Bar]
var closePath: Bool = true
func path(in rect: CGRect) -> Path {
Path { path in
let start = CGFloat(bars.first?.value ?? 0) / CGFloat(bars.max()?.value ?? 1)
path.move(to: CGPoint(x: 0, y: rect.height - rect.height * start))
let stepX = rect.width / CGFloat(bars.count)
var currentX: CGFloat = 0
bars.forEach {
currentX += stepX
let y = CGFloat($0.value / (bars.max()?.value ?? 1)) * rect.height
path.addLine(to: CGPoint(x: currentX, y: rect.height - y))
}
if closePath {
path.addLine(to: CGPoint(x: currentX, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: rect.height))
path.closeSubpath()
}
}
}
}
#if DEBUG
struct LineChartView_Previews: PreviewProvider {
static var previews: some View {
LineChartView(bars: Bar.mock)
}
}
#endif
@@ -0,0 +1,13 @@
//
// File.swift
//
//
// Created by Majid Jabrayilov on 20.07.20.
//
import Foundation
extension RandomAccessCollection {
func indexed() -> Array<(offset: Int, element: Element)> {
Array(enumerated())
}
}
@@ -1,3 +0,0 @@
struct SwiftUICharts {
var text = "Hello, World!"
}
-1
View File
@@ -1,5 +1,4 @@
import XCTest
import SwiftUIChartsTests
var tests = [XCTestCaseEntry]()
@@ -2,14 +2,7 @@ import XCTest
@testable import SwiftUICharts
final class SwiftUIChartsTests: XCTestCase {
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
XCTAssertEqual(SwiftUICharts().text, "Hello, World!")
}
static var allTests = [
("testExample", testExample),
static var allTests: [(String, () -> Void)] = [
]
}