import SwiftUI import WMF private struct SparklineShape: Shape { // MARK: Private Properties private let data: [CGFloat] // MARK: Public init(data: [NSNumber]?) { self.data = data?.compactMap { CGFloat($0.doubleValue) } ?? [] } // MARK: Shape func path(in rect: CGRect) -> Path { var path = Path() guard data.count > 1, let min = data.min(), let max = data.max() else { return path } guard min != max else { path.move(to: CGPoint(x: rect.minX, y: rect.midY)) path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY)) return path } let minY = rect.minY let width = rect.width / CGFloat(data.count - 1) let height = rect.maxY - rect.minY var points: [CGPoint] = [] for (index, dataPoint) in data.enumerated() { let relativeY = dataPoint - CGFloat(min) let normalizedY = 1 - relativeY/(CGFloat(max-min)) let y = minY + height * normalizedY let x = width * CGFloat(index) points.append(CGPoint(x: x, y: y)) } path.move(to: points[0]) for (index, point) in points[1...points.count-1].enumerated() { let fromPoint = points[index] let midPoint = CGPoint.midPointFrom(fromPoint, to: point) let midPointControlPoint = CGPoint.quadCurveControlPointFrom(midPoint, to: fromPoint) path.addQuadCurve(to: midPoint, control: midPointControlPoint) let toPointControlPoint = CGPoint.quadCurveControlPointFrom(midPoint, to: point) path.addQuadCurve(to: point, control: toPointControlPoint) } return path } } struct SparklineGrid: View { @Environment(\.colorScheme) private var colorScheme // MARK: Properties var gridStyle: Sparkline.GridStyle // MARK: View var body: some View { switch gridStyle { case .horizontal: GeometryReader { proxy in let yOffset = proxy.size.height / 2.0 Path { path in path.move(to: CGPoint(x: 0, y: yOffset)) path.addLine(to: CGPoint(x: proxy.size.width, y: yOffset)) path.move(to: CGPoint(x: 0, y: yOffset / 2.0)) path.addLine(to: CGPoint(x: proxy.size.width, y: yOffset / 2.0)) path.move(to: CGPoint(x: 0, y: yOffset * 1.5)) path.addLine(to: CGPoint(x: proxy.size.width, y: yOffset * 1.5)) } .stroke(style: StrokeStyle(lineWidth: 1.0, lineCap: .round)) } case .horizontalAndVertical: ZStack { VStack { Spacer() Rectangle().frame(height: 1) Spacer() Rectangle().frame(height: 1) Spacer() Rectangle().frame(height: 1) Spacer() Rectangle().frame(height: 1) Spacer() } HStack { Spacer() Rectangle().frame(width: 1) Spacer() Rectangle().frame(width: 1) Spacer() Rectangle().frame(width: 1) Spacer() } } } } } struct Sparkline: View { @Environment(\.colorScheme) private var colorScheme // MARK: Nested Types enum Style { case compact case compactWithViewCount case expanded } enum GridStyle { case horizontal case horizontalAndVertical } // MARK: Properties var style: Style = .compact var gridStyle: GridStyle = .horizontal var lineWidth: CGFloat { return style == .expanded ? 2.25 : 1.5 } var timeSeries: [NSNumber]? = [] var containerBackgroundColor: Color { switch style { case .compactWithViewCount: return colorScheme == .dark ? Color.white.opacity(0.12) : Color(red: 248/255.0, green: 248/255.0, blue: 250/255.0, opacity: 1) case .compact: return colorScheme == .dark ? .black : .white case .expanded: return colorScheme == .dark ? .black : .white } } var gradientStartColor: Color { colorScheme == .light ? Theme.light.colors.rankGradientStart.asColor : Theme.dark.colors.rankGradientStart.asColor } var gradientEndColor: Color { colorScheme == .light ? Theme.light.colors.rankGradientEnd.asColor : Theme.dark.colors.rankGradientEnd.asColor } // MARK: View var body: some View { if style == .compact || style == .compactWithViewCount { HStack { Spacer().frame(width: 4) ZStack { SparklineGrid(gridStyle: .horizontal) .foregroundColor(colorScheme == .dark ? Color(.sRGB, red: 55/255.0, green: 55/255.0, blue: 55/255.0, opacity: 1) : Color(.sRGB, red: 235/255.0, green: 235/255.0, blue: 235/255.0, opacity: 1) ) .layoutPriority(-1) SparklineShape(data: timeSeries) .stroke( LinearGradient(gradient: Gradient(colors: [gradientStartColor, gradientEndColor]), startPoint: /*@START_MENU_TOKEN@*/.leading/*@END_MENU_TOKEN@*/, endPoint: /*@START_MENU_TOKEN@*/.trailing/*@END_MENU_TOKEN@*/), style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round)) .frame(width: style == .compact ? 22 : 30, alignment: .leading) .padding([.top, .bottom, .leading, .trailing], 3) } if style == .compactWithViewCount { Text("\(currentViewCountOrEmpty)") .font(.system(size: 12)) .fontWeight(.medium) .foregroundColor(Theme.light.colors.rankGradientEnd.asColor) } Spacer().frame(width: 4) } .background(containerBackgroundColor) } else { ZStack { SparklineGrid(gridStyle: .horizontalAndVertical) .foregroundColor(colorScheme == .dark ? Color(.sRGB, red: 55/255.0, green: 55/255.0, blue: 55/255.0, opacity: 1) : Color(.sRGB, red: 235/255.0, green: 235/255.0, blue: 235/255.0, opacity: 1) ) SparklineShape(data: timeSeries) .stroke( LinearGradient(gradient: Gradient(colors: [gradientStartColor, gradientEndColor]), startPoint: /*@START_MENU_TOKEN@*/.leading/*@END_MENU_TOKEN@*/, endPoint: /*@START_MENU_TOKEN@*/.trailing/*@END_MENU_TOKEN@*/), style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round)) .padding([.top, .bottom, .leading, .trailing], 8) } .background(containerBackgroundColor) } } // MARK: Private private var currentViewCountOrEmpty: String { guard let currentViewCount = timeSeries?.last else { return "–" } return NumberFormatter.localizedThousandsStringFromNumber(currentViewCount) } }