折線圖是觀察某時間段內,收支狀況的好方式,可以輕鬆的觀察波動,如果有特別高的開銷也能一眼看透,工程師說太多話沒有用,來實做吧。
因為我們現在只有個別的收支記錄,但是畫折線圖,預計會需要依照「天」統計的資料,比方說 2017-10-10 當天的總花費為 250 元。
理論上是要新增一個 Entity,但因為要動到 Core Data,而且還要在把舊有的資料做統計後進行 Migration 新增統計資料,相當麻煩,這篇的主角是折線圖,所以暫時先用假資料,如下:
class TransactionStats {
var amount: NSDecimalNumber
var date: Date
var type: TransactionType
init(amount: NSDecimalNumber, date: Date, type: TransactionType) {
self.amount = amount
self.date = date
self.type = type
}
}
let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-mm-dd"
formatter.timeZone = .current
return formatter
}()
transactions = [TransactionStats]([
TransactionStats(amount: 100, date: formatter.date(from: "2017-01-01")!, type: .EXPENSE),
TransactionStats(amount: 200, date: formatter.date(from: "2017-01-02")!, type: .EXPENSE),
TransactionStats(amount: 800, date: formatter.date(from: "2017-01-03")!, type: .EXPENSE),
TransactionStats(amount: 300, date: formatter.date(from: "2017-01-04")!, type: .EXPENSE),
TransactionStats(amount: 0, date: formatter.date(from: "2017-01-05")!, type: .EXPENSE),
TransactionStats(amount: 1000, date: formatter.date(from: "2017-01-10")!, type: .EXPENSE),
TransactionStats(amount: 2000, date: formatter.date(from: "2017-01-10")!, type: .INCOME)
])
預計折線圖會有兩條線,收入(綠)、支出(紅),然後還有 XY 軸的數字(來不及做,之後再補上)。
我們會從收支統計中,找出最大、最小的日期,算出其區間總天數,然後再用 UIView 的寬度算出每天平均寬度,如此一來,我們就可以計算出任意日期所在的 X 軸位置。
// 計算一天對應的長度
let stepX = rect.width / CGFloat(dates.count)
// 用任意天數計算其對應的長度
let x = CGFloat(idx) * stepX
一樣從收支統計中,找出最大的金額,最小都是預設 0,計算出「每一元」對應的高度,如此一來,我們就可以計算出任意金額對應的 Y 軸位置。
// 每 1 元對應的 Y 軸高度
let stepY = NSDecimalNumber(value: Double(rect.height)).dividing(by: maxAmount)
// 任意金額都可以計算出其對應的高度
let y = CGFloat(truncating: transaction.amount.multiplying(by: stepY))
有了上述計算 XY 軸的基礎之後,整合起來,用 UIBezierPath 畫上去,一個充滿 Bug 與希望的折線圖就大功告成啦!
let bezierPath = UIBezierPath()
bezierPath.lineWidth = 2
bezierPath.lineCapStyle = .round
bezierPath.lineJoinStyle = .round
bezierPath.move(to: CGPoint(x: rect.minX, y: rect.maxY))
for (idx, date) in dates.enumerated() {
let day = formatter.string(from: date)
let x: CGFloat = stepX * CGFloat(idx)
var y: CGFloat = 0
if let transactionAtDate = expenses.first(where: { formatter.string(from: $0.date) == day }) {
y = CGFloat(truncating: transactionAtDate.amount.multiplying(by: stepY))
}
bezierPath.addLine(to: CGPoint(x: x + rect.minX, y: rect.maxY - y))
}
bezierPath.stroke()
程式碼:GitHub
本來開了一天的實做時間,但很明顯不夠,還得要轉 Core Data 的資料,只好明天繼續了。