|
| 1 | +import Foundation |
| 2 | +import SwiftUI |
| 3 | + |
| 4 | + |
| 5 | +/// A view that that represents the data with a series of data points called "markers" |
| 6 | +/// connected by straight line segments. It is a basic type of chart common in many |
| 7 | +/// fields. |
| 8 | +/// |
| 9 | +/// You can create a line view by providing horizontal and vertical coordinates: |
| 10 | +/// |
| 11 | +/// ```swift |
| 12 | +/// LineView( |
| 13 | +/// x: [0, 1, 2, 3, 4, 5], |
| 14 | +/// y: [0, 3, 7, 12, 18, 30] |
| 15 | +/// ) |
| 16 | +/// ``` |
| 17 | +/// Usually `LineView` is used within ``PlotView`` container that automatically defines |
| 18 | +/// axes with appropriate ticks. You can use ``PlotView/contentDisposition(minX:maxX:minY:maxY:)`` |
| 19 | +/// to adjust the limits of the axes to position the view's content as you want. |
| 20 | +/// |
| 21 | +/// ## Styling Line Views |
| 22 | +/// |
| 23 | +/// You can customize the stroke of the line within the view using |
| 24 | +/// ``LineView/lineStroke(style:)`` view modifier: |
| 25 | +/// ```swift |
| 26 | +/// PlotView { |
| 27 | +/// LineView( |
| 28 | +/// x: [1, 2, 3, 4, 5, 6] |
| 29 | +/// y: [10, 20, 5, 15, 18, 3] |
| 30 | +/// ) |
| 31 | +/// .lineStroke(style: StrokeStyle(lineWidth: 1.0, dash: [2])) |
| 32 | +/// } |
| 33 | +/// ``` |
| 34 | +/// |
| 35 | +/// You can also change the default color of the line using ``LineView/lineColor(_:)``: |
| 36 | +/// ```swift |
| 37 | +/// PlotView { |
| 38 | +/// LineView( |
| 39 | +/// x: [1, 2, 3, 4, 5, 6] |
| 40 | +/// y: [10, 20, 5, 15, 18, 3] |
| 41 | +/// ) |
| 42 | +/// .lineColor(.black) |
| 43 | +/// } |
| 44 | +/// ``` |
| 45 | +/// |
| 46 | +/// Additionally, you can modify the background overlay of the line chart using |
| 47 | +/// ``LineView/lineFill(_:)``: |
| 48 | +/// ```swift |
| 49 | +/// PlotView { |
| 50 | +/// LineView( |
| 51 | +/// x: [1, 2, 3, 4, 5, 6] |
| 52 | +/// y: [10, 20, 5, 15, 18, 3] |
| 53 | +/// ) |
| 54 | +/// .lineFill(.green.opacity(0.3)) |
| 55 | +/// } |
| 56 | +/// ``` |
| 57 | +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) |
| 58 | +public struct LineView: FuncView { |
| 59 | + |
| 60 | + private var x: [Double] |
| 61 | + private var y: [Double] |
| 62 | + private var _disposition: ContentDisposition |
| 63 | + |
| 64 | + @Environment(\.viewport) private var viewport |
| 65 | + @Environment(\.contentDisposition) private var contentDisposition |
| 66 | + |
| 67 | + private var stroke: StrokeStyle = StrokeStyle(lineWidth: 2.0) |
| 68 | + private var color: Color = .green |
| 69 | + private var fill: AnyShapeStyle = AnyShapeStyle( |
| 70 | + LinearGradient( |
| 71 | + colors: [.green.opacity(0.5), .green.opacity(0.0)], |
| 72 | + startPoint: .top, endPoint: .bottom |
| 73 | + ) |
| 74 | + ) |
| 75 | + |
| 76 | + /// The content disposition limits. |
| 77 | + public var disposition: ContentDisposition { |
| 78 | + return contentDisposition.merge(_disposition) |
| 79 | + } |
| 80 | + |
| 81 | + internal init( |
| 82 | + _ x: [Double], |
| 83 | + _ y: [Double], |
| 84 | + _ disposition: ContentDisposition |
| 85 | + ) { |
| 86 | + self.x = x |
| 87 | + self.y = y |
| 88 | + self._disposition = disposition |
| 89 | + } |
| 90 | + |
| 91 | + /// Creates a line chart at the given positions with determined height. |
| 92 | + /// |
| 93 | + /// - Parameters: |
| 94 | + /// - x: Coordinates on a horizontal axis. |
| 95 | + /// - y: Coordinates on a vertical axis. |
| 96 | + public init(x: [Double], y: [Double]) { |
| 97 | + self.x = x |
| 98 | + self.y = y |
| 99 | + |
| 100 | + self._disposition = ContentDisposition( |
| 101 | + minX: x.min(), maxX: x.max(), minY: y.min(), maxY: y.max() |
| 102 | + ) |
| 103 | + } |
| 104 | + |
| 105 | + /// The content and behaviour of the view. |
| 106 | + public var body: some View { |
| 107 | + GeometryReader { rect in |
| 108 | + let frame = viewport.inset(rect: CGRect(origin: .zero, size: rect.size)) |
| 109 | + |
| 110 | + let xScale = CGFloat(frame.width / disposition.width) |
| 111 | + let yScale = CGFloat(frame.height / disposition.height) |
| 112 | + |
| 113 | + let xZero = (disposition.width - disposition.maxX) * xScale |
| 114 | + let yZero = frame.height - (disposition.height - disposition.maxY) * yScale |
| 115 | + |
| 116 | + let minX = (x.first ?? disposition.minX) * xScale |
| 117 | + let minY = (y.first ?? disposition.minY) * yScale |
| 118 | + let maxX = (x.last ?? disposition.maxX) * xScale |
| 119 | + |
| 120 | + let line = Path { path in |
| 121 | + path.move(to: CGPoint(x: xZero + minX, y: yZero - minY)) |
| 122 | + x.indices.forEach { i in |
| 123 | + let xpos = x[i] * xScale |
| 124 | + let ypos = y[i] * yScale |
| 125 | + |
| 126 | + let x = xZero + xpos |
| 127 | + let y = yZero - ypos |
| 128 | + |
| 129 | + path.addLine(to: CGPoint(x: x, y: y)) |
| 130 | + } |
| 131 | + } |
| 132 | + .stroke(style: stroke) |
| 133 | + .fill(color) |
| 134 | + |
| 135 | + let overlay = Path { path in |
| 136 | + path.move(to: CGPoint(x: xZero + minX, y: yZero)) |
| 137 | + x.indices.forEach { i in |
| 138 | + let xpos = x[i] * xScale |
| 139 | + let ypos = y[i] * yScale |
| 140 | + |
| 141 | + let x = xZero + xpos |
| 142 | + let y = yZero - ypos |
| 143 | + |
| 144 | + path.addLine(to: CGPoint(x: x, y: y)) |
| 145 | + } |
| 146 | + path.addLine(to: CGPoint(x: xZero + maxX, y: yZero)) |
| 147 | + } |
| 148 | + |
| 149 | + overlay.fill(fill).overlay(line) |
| 150 | + } |
| 151 | + } |
| 152 | +} |
| 153 | + |
| 154 | +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) |
| 155 | +extension LineView { |
| 156 | + /// Changes the stroke style for a line within a line view. |
| 157 | + public func lineStroke(style: StrokeStyle) -> LineView { |
| 158 | + var view = self |
| 159 | + view.stroke = style |
| 160 | + return view |
| 161 | + } |
| 162 | + |
| 163 | + /// Changes the color of the line within the line view. |
| 164 | + public func lineColor(_ color: Color) -> LineView { |
| 165 | + var view = self |
| 166 | + view.color = color |
| 167 | + return view |
| 168 | + } |
| 169 | + |
| 170 | + /// Changes the color of the area under the line within the line view. |
| 171 | + public func lineFill<S: ShapeStyle>(_ style: S) -> LineView { |
| 172 | + var view = self |
| 173 | + view.fill = AnyShapeStyle(style) |
| 174 | + return view |
| 175 | + } |
| 176 | +} |
| 177 | + |
| 178 | +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) |
| 179 | +struct LineViewPreview: PreviewProvider { |
| 180 | + static var previews: some View { |
| 181 | + PlotView{ |
| 182 | + LineView( |
| 183 | + x: Array(stride(from: 0.0, to: 10.2, by: 0.2)), |
| 184 | + y: (0...100).map { _ in Double.random(in: 0..<1)} |
| 185 | + ) |
| 186 | + .lineStroke(style: StrokeStyle(lineWidth: 1)) |
| 187 | + .lineColor(.blue) |
| 188 | + .lineFill(Color.blue.opacity(0.3)) |
| 189 | + } |
| 190 | + .tickColor(.white) |
| 191 | + .tickInsets(bottom: 20, trailing: 20) |
| 192 | + .contentDisposition(minX: -5, maxX: 15, minY: 0) |
| 193 | + .padding(50) |
| 194 | + .frame(width: 600, height: 300) |
| 195 | + .foregroundColor(.white.opacity(0.8)) |
| 196 | + .background(Color.black) |
| 197 | + } |
| 198 | +} |
0 commit comments