210 lines
6.9 KiB
Swift
210 lines
6.9 KiB
Swift
//
|
|
// LineGraphView.swift
|
|
// vcam
|
|
//
|
|
// Created by TensorWorks on 17/7/2023.
|
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
class LineGraphView: UIView {
|
|
|
|
// An array of data points for the graph
|
|
private var dataPoints: [CGFloat] = []
|
|
|
|
// Limit to the number of data points stored in the array
|
|
private var sizeLimit: UInt16 = 120
|
|
|
|
// Font to use in our graph title
|
|
private var titleFont: UIFont
|
|
|
|
// Font to use in our graph tick labels
|
|
private var tickLabelFont: UIFont
|
|
|
|
// Color of our font
|
|
private var fontColor: UIColor = UIColor.white
|
|
|
|
// Paragraph style for our text
|
|
private var paragraphStyle: NSMutableParagraphStyle = NSMutableParagraphStyle()
|
|
|
|
// Title text attributes
|
|
private var titleTextAttributes: [NSAttributedString.Key: Any] = [:]
|
|
|
|
// Tick label text attributes
|
|
private var tickTextAttributes: [NSAttributedString.Key: Any] = [:]
|
|
|
|
// Maximum y value we have seen
|
|
public var maxY: CGFloat = 1
|
|
|
|
// Title of graph
|
|
public var title: String = "Title" {
|
|
didSet {
|
|
setNeedsDisplay() // Trigger redraw when the text is set
|
|
}
|
|
}
|
|
|
|
// Function for formatting the numberic text labels next to the y-Axis ticks
|
|
public var yTickLabelFormatter : (Double) -> String = {
|
|
return String(format: "%.0f", round($0))
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
// Font to use in our graph title
|
|
self.titleFont = UIFont.systemFont(ofSize: 12 / (UIScreen.main.scale - 1));
|
|
|
|
// Font to use in our graph tick labels
|
|
self.tickLabelFont = UIFont.systemFont(ofSize: 8 / (UIScreen.main.scale - 1));
|
|
|
|
super.init(frame: frame)
|
|
|
|
// set transparent background color
|
|
self.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.5)
|
|
|
|
// fill data points with 1's
|
|
for _ in 1...self.sizeLimit {
|
|
addDataPoint(value: CGFloat(0));
|
|
}
|
|
|
|
// Set alignment of text in our graphs to be center
|
|
self.paragraphStyle.alignment = .left;
|
|
|
|
// Set the attributes for the text
|
|
self.titleTextAttributes = [
|
|
.font: self.titleFont,
|
|
.foregroundColor: self.fontColor,
|
|
.paragraphStyle: self.paragraphStyle
|
|
]
|
|
|
|
self.tickTextAttributes = [
|
|
.font: self.tickLabelFont,
|
|
.foregroundColor: self.fontColor,
|
|
.paragraphStyle: self.paragraphStyle
|
|
]
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
// Font to use in our graph title
|
|
self.titleFont = UIFont.systemFont(ofSize: 12 / (UIScreen.main.scale - 1));
|
|
|
|
// Font to use in our graph tick labels
|
|
self.tickLabelFont = UIFont.systemFont(ofSize: 8 / (UIScreen.main.scale - 1));
|
|
|
|
super.init(coder: coder)
|
|
self.backgroundColor = UIColor.clear
|
|
}
|
|
|
|
func addDataPoint(value: CGFloat) {
|
|
|
|
if value > self.maxY || value == self.maxY {
|
|
// adds some headroom the the graph
|
|
self.maxY = value + (self.maxY * 0.1)
|
|
}
|
|
|
|
self.dataPoints.append(value)
|
|
|
|
if self.dataPoints.count > self.sizeLimit {
|
|
self.dataPoints.removeFirst();
|
|
}
|
|
|
|
// Triggers a redraw (need to do this from main thread)
|
|
DispatchQueue.main.async {
|
|
if !self.isHidden && self.window != nil {
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
}
|
|
|
|
override func draw(_ rect: CGRect) {
|
|
super.draw(rect)
|
|
|
|
guard let context = UIGraphicsGetCurrentContext() else { return }
|
|
|
|
// Draw the text in the bounding rectangle
|
|
let titleSize = self.title.size(withAttributes: self.titleTextAttributes);
|
|
let titleRect = CGRect(
|
|
x: (rect.width - titleSize.width) * 0.5,
|
|
y: 0,
|
|
width: rect.width,
|
|
height: rect.height
|
|
)
|
|
self.title.draw(in: titleRect, withAttributes: self.titleTextAttributes)
|
|
|
|
|
|
// Draw the y-axis
|
|
let axisPadding = rect.width * 0.125;
|
|
UIColor.white.setStroke()
|
|
let yAxis = UIBezierPath()
|
|
yAxis.lineWidth = 1.0
|
|
yAxis.move(to: CGPoint(x: axisPadding, y: titleSize.height))
|
|
yAxis.addLine(to: CGPoint(x: axisPadding, y: rect.height))
|
|
yAxis.stroke()
|
|
|
|
// Draw y-axis ticks
|
|
let tickLength = 3.0
|
|
let yAxisHeight = rect.height - titleSize.height
|
|
let nTicks = 5
|
|
|
|
for tickYIdx in 0...nTicks {
|
|
let tickPath = UIBezierPath();
|
|
tickPath.lineWidth = 1.0
|
|
let scalar = CGFloat(tickYIdx) / CGFloat(nTicks)
|
|
let tickY = titleSize.height + (scalar * yAxisHeight)
|
|
tickPath.move(to: CGPoint(x: axisPadding, y: tickY))
|
|
tickPath.addLine(to: CGPoint(x: axisPadding - tickLength, y: tickY))
|
|
tickPath.stroke()
|
|
// Draw tick label
|
|
let tickValue : CGFloat = self.maxY - ( self.maxY * scalar )
|
|
let tickLabel = self.yTickLabelFormatter(tickValue)
|
|
|
|
let tickLabelSize = tickLabel.size(withAttributes: self.tickTextAttributes);
|
|
let tickLabelX : CGFloat = axisPadding - tickLength - tickLabelSize.width
|
|
let tickLabelYOffset = tickYIdx == nTicks ? tickLabelSize.height : tickLabelSize.height * 0.5
|
|
let tickLabelY: CGFloat = tickY - tickLabelYOffset
|
|
let tickRect = CGRect(x: tickLabelX, y: tickLabelY, width: rect.width, height: rect.height)
|
|
tickLabel.draw(in: tickRect, withAttributes: self.tickTextAttributes)
|
|
}
|
|
|
|
// Draw the line graph
|
|
let graphYSize = rect.height - titleSize.height
|
|
let graphXSize = rect.width - axisPadding
|
|
|
|
// Set the line color
|
|
UIColor.red.setStroke()
|
|
|
|
// Set up the coordinate system
|
|
let coordinateSystem = CGRect(x: rect.origin.x, y: rect.origin.y, width: graphXSize, height: graphYSize)
|
|
context.saveGState()
|
|
context.translateBy(x: axisPadding, y: coordinateSystem.height + titleSize.height)
|
|
context.scaleBy(x: 1, y: -1)
|
|
|
|
// Calculate the scale factors
|
|
let maxX = CGFloat(dataPoints.count)
|
|
let scaleX = coordinateSystem.width / maxX
|
|
let scaleY = coordinateSystem.height / self.maxY
|
|
|
|
// Create the line path
|
|
let path = UIBezierPath()
|
|
path.lineWidth = 1.0
|
|
|
|
for (index, dataPoint) in dataPoints.enumerated() {
|
|
let x = CGFloat(index) * scaleX
|
|
let y = dataPoint * scaleY
|
|
|
|
if index == 0 {
|
|
path.move(to: CGPoint(x: x, y: y))
|
|
} else {
|
|
path.addLine(to: CGPoint(x: x, y: y))
|
|
}
|
|
}
|
|
|
|
// Draw the line
|
|
path.stroke()
|
|
|
|
context.restoreGState()
|
|
}
|
|
|
|
}
|
|
|