charting code added to the package
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,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)] = [
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user