From f2d9a491faded6243aef50162c3167246c04f2b4 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Mon, 8 Apr 2024 16:44:26 +0200 Subject: [PATCH] feat: added a new NetworkChartViewV2 chart. Which merges 2 line charts. Moved the disk and network charts in the popup to the new chart. --- Kit/Widgets/NetworkChart.swift | 5 +- Kit/plugins/Charts.swift | 299 +++++++++++++++------------------ Modules/Disk/popup.swift | 4 +- Modules/Disk/portal.swift | 4 +- Modules/Net/popup.swift | 7 +- Modules/Net/portal.swift | 4 +- Stats/Views/AppSettings.swift | 1 - 7 files changed, 149 insertions(+), 175 deletions(-) diff --git a/Kit/Widgets/NetworkChart.swift b/Kit/Widgets/NetworkChart.swift index 873dfbd1..a3744ed9 100644 --- a/Kit/Widgets/NetworkChart.swift +++ b/Kit/Widgets/NetworkChart.swift @@ -22,10 +22,7 @@ public class NetworkChart: WidgetWrapper { private var commonScaleState: Bool = true private var reverseOrderState: Bool = false - private var chart: NetworkChartView = NetworkChartView( - frame: NSRect(x: 0, y: 0, width: 30, height: Constants.Widget.height - (2*Constants.Widget.margin.y)), - num: 60, minMax: false, toolTip: false - ) + private var chart: NetworkChartView = NetworkChartView(frame: NSRect(x: 0, y: 0, width: 30, height: Constants.Widget.height-(2*Constants.Widget.margin.y)), num: 60) private var width: CGFloat { get { switch self.historyCount { diff --git a/Kit/plugins/Charts.swift b/Kit/plugins/Charts.swift index 8866cd2c..150065a0 100644 --- a/Kit/plugins/Charts.swift +++ b/Kit/plugins/Charts.swift @@ -21,7 +21,7 @@ public struct circle_segment { } } -private func scaleValue(scale: Scale = .linear, value: Double, maxValue: Double, maxHeight: CGFloat, limit: Double) -> CGFloat { +public func scaleValue(scale: Scale = .linear, value: Double, maxValue: Double, maxHeight: CGFloat, limit: Double) -> CGFloat { var value = value if scale == .none && value > 1 && maxValue != 0 { value /= maxValue @@ -87,6 +87,9 @@ private func drawToolTip(_ frame: NSRect, _ point: CGPoint, _ size: CGSize, valu if position.y + textHeight > size.height { position.y = point.y - textHeight - 20 } + if position.y < 2 { + position.y = 2 + } let box = NSBezierPath(roundedRect: NSRect(x: position.x-3, y: position.y-2, width: size.width, height: textHeight+2), xRadius: 2, yRadius: 2) NSColor.gray.setStroke() @@ -98,7 +101,7 @@ private func drawToolTip(_ frame: NSRect, _ point: CGPoint, _ size: CGSize, valu NSAttributedString.Key.font: NSFont.systemFont(ofSize: 12, weight: .regular), NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor ] - var rect = CGRect(x: position.x, y: position.y+valueOffset, width: 32, height: 12) + var rect = CGRect(x: position.x, y: position.y+valueOffset, width: size.width, height: 12) var str = NSAttributedString.init(string: value, attributes: attributes) str.draw(with: rect) @@ -114,21 +117,27 @@ private func drawToolTip(_ frame: NSRect, _ point: CGPoint, _ size: CGSize, valu public class LineChartView: NSView { public var id: String = UUID().uuidString + private let dateFormatter = DateFormatter() + public var points: [DoubleValue?] public var shadowPoints: [DoubleValue?] = [] public var transparent: Bool = true - public var color: NSColor = .controlAccentColor - public var suffix: String = "%" + public var flipY: Bool = false + public var minMax: Bool = false + public var color: NSColor + public var suffix: String + public var toolTipFunc: ((DoubleValue) -> String)? private var scale: Scale private var fixedScale: Double private var cursor: NSPoint? = nil private var stop: Bool = false - private let dateFormatter = DateFormatter() - public init(frame: NSRect, num: Int, scale: Scale = .none, fixedScale: Double = 1) { + public init(frame: NSRect, num: Int, suffix: String = "", color: NSColor = .controlAccentColor, scale: Scale = .none, fixedScale: Double = 1) { self.points = Array(repeating: nil, count: num) + self.suffix = suffix + self.color = color self.scale = scale self.fixedScale = fixedScale @@ -166,8 +175,9 @@ public class LineChartView: NSView { } let offset: CGFloat = 1 / (NSScreen.main?.backingScaleFactor ?? 1) - let height: CGFloat = self.frame.height - dirtyRect.origin.y - offset + let height: CGFloat = self.frame.height - offset let xRatio: CGFloat = self.frame.width / CGFloat(points.count-1) + let zero: CGFloat = self.flipY ? self.frame.height : 0 var lines: [[CGPoint]] = [] var line: [CGPoint] = [] @@ -181,9 +191,15 @@ public class LineChartView: NSView { } continue } + + var y = scaleValue(scale: self.scale, value: v.value, maxValue: maxValue, maxHeight: height, limit: self.fixedScale) + if self.flipY { + y = height - y + } + let point = CGPoint( x: (CGFloat(i) * xRatio) + dirtyRect.origin.x, - y: scaleValue(scale: self.scale, value: v.value, maxValue: maxValue, maxHeight: height, limit: self.fixedScale) + dirtyRect.origin.y + offset + y: y ) line.append(point) list.append((value: v, point: point)) @@ -213,13 +229,37 @@ public class LineChartView: NSView { path.stroke() path = path.copy() as! NSBezierPath - path.line(to: CGPoint(x: linePoints[linePoints.count-1].x, y: 0)) - path.line(to: CGPoint(x: linePoints[0].x, y: 0)) + path.line(to: CGPoint(x: linePoints[linePoints.count-1].x, y: zero)) + path.line(to: CGPoint(x: linePoints[0].x, y: zero)) path.close() gradientColor.set() path.fill() } + if self.minMax { + let stringAttributes = [ + NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .light), + NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor, + NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle() + ] + + var str: String = "" + let flatList = self.points.map{ $0?.value ?? 0 } + if let value = flatList.min() { + str += self.toolTipFunc != nil ? self.toolTipFunc!(DoubleValue(value)) : "\(Int(value.rounded(toPlaces: 2) * 100))\(self.suffix)" + } + if let value = flatList.max() { + if !str.isEmpty { + str += " / " + } + str += self.toolTipFunc != nil ? self.toolTipFunc!(DoubleValue(value)) : "\(Int(value.rounded(toPlaces: 2) * 100))\(self.suffix)" + } + let textWidth = str.widthOfString(usingFont: stringAttributes[NSAttributedString.Key.font] as! NSFont) + let y = self.flipY ? 1 : height - 9 + let rect = CGRect(x: 1, y: y, width: textWidth, height: 8) + NSAttributedString.init(string: str, attributes: stringAttributes).draw(with: rect) + } + if let p = self.cursor, let over = list.first(where: { $0.point.x >= p.x }), let under = list.last(where: { $0.point.x <= p.x }) { guard p.y <= height else { return } @@ -237,7 +277,7 @@ public class LineChartView: NSView { vLine.close() hLine.move(to: CGPoint(x: 0, y: p.y)) - hLine.line(to: CGPoint(x: self.frame.size.width+self.frame.origin.x, y: p.y)) + hLine.line(to: CGPoint(x: self.frame.size.width, y: p.y)) hLine.close() NSColor.tertiaryLabelColor.set() @@ -259,7 +299,7 @@ public class LineChartView: NSView { path.stroke() let date = self.dateFormatter.string(from: nearest.value.ts) - let value = "\(Int(nearest.value.value.rounded(toPlaces: 2) * 100))\(self.suffix)" + let value = self.toolTipFunc != nil ? self.toolTipFunc!(nearest.value) : "\(Int(nearest.value.value.rounded(toPlaces: 2) * 100))\(self.suffix)" drawToolTip(self.frame, CGPoint(x: nearest.point.x+4, y: nearest.point.y+4), CGSize(width: 78, height: height), value: value, subtitle: date) } } @@ -340,43 +380,110 @@ public class LineChartView: NSView { } } +public class NetworkChartViewV2: NSView { + public var id: String = UUID().uuidString + + public var base: DataSizeBase = .byte + + private var reversedOrder: Bool + + private var inChart: LineChartView + private var outChart: LineChartView + + public init(frame: NSRect, num: Int, minMax: Bool = true, reversedOrder: Bool = false, + outColor: NSColor = .systemRed, inColor: NSColor = .systemBlue, scale: Scale = .none, fixedScale: Double = 1) { + self.reversedOrder = reversedOrder + + let topFrame = NSRect(x: 0, y: frame.height/2, width: frame.width, height: frame.height/2) + let bottomFrame = NSRect(x: 0, y: 0, width: frame.width, height: frame.height/2) + let inFrame = self.reversedOrder ? topFrame : bottomFrame + let outFrame = self.reversedOrder ? bottomFrame : topFrame + self.inChart = LineChartView(frame: inFrame, num: num, color: inColor, scale: scale, fixedScale: fixedScale) + self.outChart = LineChartView(frame: outFrame, num: num, color: outColor, scale: scale, fixedScale: fixedScale) + + super.init(frame: frame) + + self.inChart.minMax = minMax + self.outChart.minMax = minMax + + self.inChart.flipY = !self.reversedOrder + self.outChart.flipY = self.reversedOrder + + self.inChart.toolTipFunc = { v in + return Units(bytes: Int64(v.value)).getReadableSpeed(base: self.base) + } + self.outChart.toolTipFunc = { v in + return Units(bytes: Int64(v.value)).getReadableSpeed(base: self.base) + } + + self.addSubview(self.inChart) + self.addSubview(self.outChart) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func addValue(upload: Double, download: Double) { + self.inChart.addValue(DoubleValue(upload)) + self.outChart.addValue(DoubleValue(download)) + } + + public func reinit(_ num: Int = 60) { + self.inChart.reinit(num) + self.outChart.reinit(num) + } + + public func setScale(_ newScale: Scale, _ fixedScale: Double = 1) { + self.inChart.setScale(newScale, fixedScale: fixedScale) + self.outChart.setScale(newScale, fixedScale: fixedScale) + } + + public func setReverseOrder(_ newValue: Bool) { + guard self.reversedOrder != newValue else { return } + self.reversedOrder = newValue + + self.inChart.flipY = !self.reversedOrder + self.outChart.flipY = self.reversedOrder + + let topFrame = CGPoint(x: 0, y: frame.height/2) + let bottomFrame = CGPoint(x: 0, y: 0) + self.inChart.setFrameOrigin(self.reversedOrder ? topFrame : bottomFrame) + self.outChart.setFrameOrigin(self.reversedOrder ? bottomFrame : topFrame) + + self.inChart.display() + self.outChart.display() + } + + public func setColors(in inColor: NSColor? = nil, out outColor: NSColor? = nil) { + if let inColor { + self.inChart.color = inColor + } + if let outColor { + self.outChart.color = outColor + } + } +} + public class NetworkChartView: NSView { public var id: String = UUID().uuidString public var base: DataSizeBase = .byte public var topColor: NSColor public var bottomColor: NSColor public var points: [(Double, Double)] - public var shadowPoints: [(Double, Double)] = [] - private var minMax: Bool = false private var scale: Scale = .none private var fixedScale: Double private var commonScale: Bool = true private var reverseOrder: Bool = false - private var cursorToolTip: Bool = false - private var cursor: NSPoint? = nil - private var stop: Bool = false - - public init(frame: NSRect, num: Int, minMax: Bool = true, outColor: NSColor = .systemRed, inColor: NSColor = .systemBlue, toolTip: Bool = false, scale: Scale = .none, fixedScale: Double = 1) { - self.minMax = minMax + public init(frame: NSRect, num: Int, outColor: NSColor = .systemRed, inColor: NSColor = .systemBlue, scale: Scale = .none, fixedScale: Double = 1) { self.points = Array(repeating: (0, 0), count: num) self.topColor = inColor self.bottomColor = outColor - self.cursorToolTip = toolTip self.scale = scale self.fixedScale = fixedScale super.init(frame: frame) - - self.addTrackingArea(NSTrackingArea( - rect: CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height), - options: [ - NSTrackingArea.Options.activeAlways, - NSTrackingArea.Options.mouseEnteredAndExited, - NSTrackingArea.Options.mouseMoved - ], - owner: self, userInfo: nil - )) } required init?(coder: NSCoder) { @@ -389,10 +496,7 @@ public class NetworkChartView: NSView { guard let context = NSGraphicsContext.current?.cgContext else { return } context.setShouldAntialias(true) - var points = self.points - if self.stop { - points = self.shadowPoints - } + let points = self.points var topMax: Double = (self.reverseOrder ? points.map{ $0.1 }.max() : points.map{ $0.0 }.max()) ?? 0 var bottomMax: Double = (self.reverseOrder ? points.map{ $0.0 }.max() : points.map{ $0.1 }.max()) ?? 0 if topMax == 0 { @@ -414,7 +518,6 @@ public class NetworkChartView: NSView { let zero: CGFloat = (self.frame.height/2) + self.frame.origin.y let xRatio: CGFloat = (self.frame.width + (lineWidth*3)) / CGFloat(points.count) let xCenter: CGFloat = self.frame.height/2 + self.frame.origin.y - let height: CGFloat = self.frame.height - dirtyRect.origin.y - lineWidth let columnXPoint = { (point: Int) -> CGFloat in return (CGFloat(point) * xRatio) + (self.frame.origin.x - lineWidth) @@ -429,13 +532,6 @@ public class NetworkChartView: NSView { return xCenter - scaleValue(scale: self.scale, value: value, maxValue: bottomMax, maxHeight: self.frame.height/2, limit: self.fixedScale) } - let topList = points.enumerated().compactMap { (i: Int, v: (Double, Double)) -> (value: Double, point: CGPoint) in - return (self.reverseOrder ? v.1 : v.0, CGPoint(x: columnXPoint(i), y: topYPoint(i))) - } - let bottomList = points.enumerated().compactMap { (i: Int, v: (Double, Double)) -> (value: Double, point: CGPoint) in - return (self.reverseOrder ? v.0 : v.1, CGPoint(x: columnXPoint(i), y: bottomYPoint(i))) - } - let topLinePath = NSBezierPath() topLinePath.move(to: CGPoint(x: columnXPoint(0), y: topYPoint(0))) @@ -478,89 +574,6 @@ public class NetworkChartView: NSView { underLinePath.addClip() topColor.withAlphaComponent(0.5).setFill() NSBezierPath(rect: self.frame).fill() - - context.restoreGState() - - if self.minMax { - let stringAttributes = [ - NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .light), - NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor, - NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle() - ] - let topText = Units(bytes: Int64(topMax)).getReadableSpeed(base: self.base) - let bottomText = Units(bytes: Int64(bottomMax)).getReadableSpeed(base: self.base) - let topTextWidth = topText.widthOfString(usingFont: stringAttributes[NSAttributedString.Key.font] as! NSFont) - let bottomTextWidth = bottomText.widthOfString(usingFont: stringAttributes[NSAttributedString.Key.font] as! NSFont) - - var rect = CGRect(x: 1, y: self.frame.height - 9, width: topTextWidth, height: 8) - NSAttributedString.init(string: topText, attributes: stringAttributes).draw(with: rect) - - rect = CGRect(x: 1, y: 2, width: bottomTextWidth, height: 8) - NSAttributedString.init(string: bottomText, attributes: stringAttributes).draw(with: rect) - } - - if let p = self.cursor { - let list = p.y > xCenter ? topList : bottomList - if let over = list.first(where: { $0.point.x >= p.x }), let under = list.last(where: { $0.point.x <= p.x }) { - let diffOver = over.point.x - p.x - let diffUnder = p.x - under.point.x - let nearest = (diffOver < diffUnder) ? over : under - let vLine = NSBezierPath() - let hLine = NSBezierPath() - - vLine.setLineDash([4, 4], count: 2, phase: 0) - hLine.setLineDash([6, 6], count: 2, phase: 0) - - vLine.move(to: CGPoint(x: p.x, y: 0)) - vLine.line(to: CGPoint(x: p.x, y: height)) - vLine.close() - - hLine.move(to: CGPoint(x: 0, y: p.y)) - hLine.line(to: CGPoint(x: self.frame.size.width+self.frame.origin.x, y: p.y)) - hLine.close() - - NSColor.tertiaryLabelColor.set() - - vLine.lineWidth = lineWidth - hLine.lineWidth = lineWidth - - vLine.stroke() - hLine.stroke() - - let dotSize: CGFloat = 4 - let path = NSBezierPath(ovalIn: CGRect( - x: nearest.point.x-(dotSize/2), - y: nearest.point.y-(dotSize/2), - width: dotSize, - height: dotSize - )) - NSColor.red.set() - path.stroke() - - let style = NSMutableParagraphStyle() - style.alignment = .left - var textPosition: CGPoint = CGPoint(x: nearest.point.x+4, y: nearest.point.y+4) - - if textPosition.x + 35 > self.frame.size.width+self.frame.origin.x { - textPosition.x = nearest.point.x - 55 - style.alignment = .right - } - if textPosition.y + 14 > height { - textPosition.y = nearest.point.y - 14 - } - - let stringAttributes = [ - NSAttributedString.Key.font: NSFont.systemFont(ofSize: 10, weight: .regular), - NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor, - NSAttributedString.Key.paragraphStyle: style - ] - - let rect = CGRect(x: textPosition.x, y: textPosition.y, width: 52, height: 10) - let value = Units(bytes: Int64(nearest.value)).getReadableSpeed(base: self.base) - let str = NSAttributedString.init(string: value, attributes: stringAttributes) - str.draw(with: rect) - } - } } public func addValue(upload: Double, download: Double) { @@ -618,40 +631,6 @@ public class NetworkChartView: NSView { self.display() } } - - public override func mouseEntered(with event: NSEvent) { - guard self.cursorToolTip else { return } - self.cursor = convert(event.locationInWindow, from: nil) - self.needsDisplay = true - } - - public override func mouseMoved(with event: NSEvent) { - guard self.cursorToolTip else { return } - self.cursor = convert(event.locationInWindow, from: nil) - self.needsDisplay = true - } - - public override func mouseDragged(with event: NSEvent) { - guard self.cursorToolTip else { return } - self.cursor = convert(event.locationInWindow, from: nil) - self.needsDisplay = true - } - - public override func mouseExited(with event: NSEvent) { - guard self.cursorToolTip else { return } - self.cursor = nil - self.needsDisplay = true - } - - public override func mouseDown(with: NSEvent) { - guard self.cursorToolTip else { return } - self.shadowPoints = self.points - self.stop = true - } - public override func mouseUp(with: NSEvent) { - guard self.cursorToolTip else { return } - self.stop = false - } } public class PieChartView: NSView { diff --git a/Modules/Disk/popup.swift b/Modules/Disk/popup.swift index a507ee43..12a46ebd 100644 --- a/Modules/Disk/popup.swift +++ b/Modules/Disk/popup.swift @@ -420,7 +420,7 @@ internal class NameView: NSStackView { } internal class ChartView: NSStackView { - private var chart: NetworkChartView? = nil + private var chart: NetworkChartViewV2? = nil private var ready: Bool = false private var readColor: NSColor { @@ -436,7 +436,7 @@ internal class ChartView: NSStackView { self.wantsLayer = true self.layer?.cornerRadius = 3 - let chart = NetworkChartView(frame: NSRect( + let chart = NetworkChartViewV2(frame: NSRect( x: 0, y: 1, width: self.frame.width, diff --git a/Modules/Disk/portal.swift b/Modules/Disk/portal.swift index 28916db0..863541c6 100644 --- a/Modules/Disk/portal.swift +++ b/Modules/Disk/portal.swift @@ -14,7 +14,7 @@ import Kit public class Portal: PortalWrapper { private var circle: PieChartView? = nil - private var chart: NetworkChartView? = nil + private var chart: NetworkChartViewV2? = nil private var nameField: NSTextField? = nil private var usedField: NSTextField? = nil @@ -91,7 +91,7 @@ public class Portal: PortalWrapper { self.usedField = portalRow(view, title: "\(localizedString("Used")):") self.freeField = portalRow(view, title: "\(localizedString("Free")):") - let chart = NetworkChartView(frame: NSRect.zero, num: 120, minMax: false, outColor: self.writeColor, inColor: self.readColor) + let chart = NetworkChartViewV2(frame: NSRect.zero, num: 120, minMax: false, outColor: self.writeColor, inColor: self.readColor) chart.heightAnchor.constraint(equalToConstant: 26).isActive = true self.chart = chart view.addArrangedSubview(chart) diff --git a/Modules/Net/popup.swift b/Modules/Net/popup.swift index faa6ed43..65a01ab2 100644 --- a/Modules/Net/popup.swift +++ b/Modules/Net/popup.swift @@ -55,7 +55,7 @@ internal class Popup: PopupWrapper { private var processesInitialized: Bool = false private var connectionInitialized: Bool = false - private var chart: NetworkChartView? = nil + private var chart: NetworkChartViewV2? = nil private var chartScale: Scale = .none private var connectivityChart: GridChartView? = nil private var processes: ProcessesView? = nil @@ -175,11 +175,10 @@ internal class Popup: PopupWrapper { container.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.1).cgColor container.layer?.cornerRadius = 3 - let chart = NetworkChartView( + let chart = NetworkChartViewV2( frame: NSRect(x: 0, y: 1, width: container.frame.width, height: container.frame.height - 2), - num: 120, outColor: self.uploadColor, inColor: self.downloadColor, toolTip: true, scale: self.chartScale + num: 120, reversedOrder: self.reverseOrderState, outColor: self.uploadColor, inColor: self.downloadColor, scale: self.chartScale ) - chart.setReverseOrder(self.reverseOrderState) chart.base = self.base container.addSubview(chart) self.chart = chart diff --git a/Modules/Net/portal.swift b/Modules/Net/portal.swift index 5ffbd29f..ef373349 100644 --- a/Modules/Net/portal.swift +++ b/Modules/Net/portal.swift @@ -13,7 +13,7 @@ import Cocoa import Kit public class Portal: PortalWrapper { - private var chart: NetworkChartView? = nil + private var chart: NetworkChartViewV2? = nil private var publicIPField: NSTextField? = nil @@ -73,7 +73,7 @@ public class Portal: PortalWrapper { view.orientation = .vertical view.distribution = .fill view.spacing = Constants.Popup.spacing*2 - let chart = NetworkChartView(frame: NSRect.zero, num: 120, minMax: true, outColor: self.uploadColor, inColor: self.downloadColor) + let chart = NetworkChartViewV2(frame: NSRect.zero, num: 120, outColor: self.uploadColor, inColor: self.downloadColor) self.chart = chart view.addArrangedSubview(chart) diff --git a/Stats/Views/AppSettings.swift b/Stats/Views/AppSettings.swift index 5337455c..5e1651dd 100644 --- a/Stats/Views/AppSettings.swift +++ b/Stats/Views/AppSettings.swift @@ -432,7 +432,6 @@ class ApplicationSettings: NSStackView { @objc private func toggleDock(_ sender: NSButton) { let state = sender.state - Store.shared.set(key: "dockIcon", value: state == NSControl.StateValue.on) let dockIconStatus = state == NSControl.StateValue.on ? NSApplication.ActivationPolicy.regular : NSApplication.ActivationPolicy.accessory NSApp.setActivationPolicy(dockIconStatus)