From dacbdc6f98b398b099e4807ebcd8a071b1a7808a Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Tue, 16 Jan 2024 21:38:38 +0100 Subject: [PATCH] feat: added value tooltip to the network chart --- Kit/plugins/Charts.swift | 147 +++++++++++++++++++++++++++++++++++---- 1 file changed, 132 insertions(+), 15 deletions(-) diff --git a/Kit/plugins/Charts.swift b/Kit/plugins/Charts.swift index 8c1d029a..a3e87664 100644 --- a/Kit/plugins/Charts.swift +++ b/Kit/plugins/Charts.swift @@ -293,18 +293,32 @@ public class NetworkChartView: NSView { 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 commonScale: Bool = true private var reverseOrder: 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) { self.minMax = minMax self.points = Array(repeating: (0, 0), count: num) self.topColor = inColor self.bottomColor = outColor 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) { @@ -317,7 +331,10 @@ public class NetworkChartView: NSView { guard let context = NSGraphicsContext.current?.cgContext else { return } context.setShouldAntialias(true) - let points = self.points + var points = self.points + if self.stop { + points = self.shadowPoints + } 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 { @@ -338,6 +355,8 @@ public class NetworkChartView: NSView { let lineWidth = 1 / (NSScreen.main?.backingScaleFactor ?? 1) 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) @@ -345,38 +364,45 @@ public class NetworkChartView: NSView { let topYPoint = { (point: Int) -> CGFloat in let value = self.reverseOrder ? points[point].1 : points[point].0 - return scaleValue(scale: self.scale, value: value, maxValue: topMax, maxHeight: self.frame.height/2) + (self.frame.height/2 + self.frame.origin.y) + return scaleValue(scale: self.scale, value: value, maxValue: topMax, maxHeight: self.frame.height/2) + xCenter } let bottomYPoint = { (point: Int) -> CGFloat in let value = self.reverseOrder ? points[point].0 : points[point].1 - return (self.frame.height/2 + self.frame.origin.y) - scaleValue(scale: self.scale, value: value, maxValue: bottomMax, maxHeight: self.frame.height/2) + return xCenter - scaleValue(scale: self.scale, value: value, maxValue: bottomMax, maxHeight: self.frame.height/2) } - let uploadlinePath = NSBezierPath() - uploadlinePath.move(to: CGPoint(x: columnXPoint(0), y: topYPoint(0))) + 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 downloadlinePath = NSBezierPath() - downloadlinePath.move(to: CGPoint(x: columnXPoint(0), y: bottomYPoint(0))) + let topLinePath = NSBezierPath() + topLinePath.move(to: CGPoint(x: columnXPoint(0), y: topYPoint(0))) + + let bottomLinePath = NSBezierPath() + bottomLinePath.move(to: CGPoint(x: columnXPoint(0), y: bottomYPoint(0))) for i in 1.. 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 + 24 > self.frame.size.width+self.frame.origin.x { + textPosition.x = nearest.point.x - 30 + 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: 26, 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) { @@ -470,6 +559,34 @@ public class NetworkChartView: NSView { self.display() } } + + public override func mouseEntered(with event: NSEvent) { + self.cursor = convert(event.locationInWindow, from: nil) + self.needsDisplay = true + } + + public override func mouseMoved(with event: NSEvent) { + self.cursor = convert(event.locationInWindow, from: nil) + self.needsDisplay = true + } + + public override func mouseDragged(with event: NSEvent) { + self.cursor = convert(event.locationInWindow, from: nil) + self.needsDisplay = true + } + + public override func mouseExited(with event: NSEvent) { + self.cursor = nil + self.needsDisplay = true + } + + public override func mouseDown(with: NSEvent) { + self.shadowPoints = self.points + self.stop = true + } + public override func mouseUp(with: NSEvent) { + self.stop = false + } } public class PieChartView: NSView {