Skip to content

Commit f8f6217

Browse files
authored
LineView plot (#44)
This patch introduces implementation of a line chart as `LineView`. See the attached screenshot as a reference example of such a chart.
1 parent dd719fe commit f8f6217

File tree

2 files changed

+199
-1
lines changed

2 files changed

+199
-1
lines changed

Sources/PlotUI/Bar.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ public struct BarView: FuncView {
153153
}
154154

155155
extension BarView {
156-
/// Modifies the corner radius fof the bars within the bar view.
156+
/// Modifies the corner radius for the bars within the bar view.
157157
public func barCornerRadius(_ radius: CGFloat) -> BarView {
158158
var view = self
159159
view.radius = radius

Sources/PlotUI/Line.swift

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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

Comments
 (0)