Skip to content

Commit 6483eae

Browse files
WilliamHYZhangKarthikRIyer
authored andcommitted
Add Arrow Features (#102)
* add arrow features * add arrow styles, tests * bfix, update test
1 parent d3db154 commit 6483eae

11 files changed

+440
-23
lines changed

Sources/SwiftPlot/PlotStyleHelpers.swift

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -165,34 +165,69 @@ struct Arrow : Annotation {
165165
public var headLength: Float = 10
166166
public var headAngle: Float = 20
167167
public var isDashed: Bool = false
168-
public var isFilled: Bool = false
168+
public var isDoubleHeaded: Bool = false
169+
public enum HeadStyle {
170+
case skeletal
171+
case filled
172+
case wedge
173+
case dart
174+
}
175+
public var headStyle = HeadStyle.skeletal
169176
public var startAnnotation: Annotation?
170177
public var endAnnotation: Annotation?
171178
public var overrideAnchor: Bool = false
172-
public mutating func draw(resolver: CoordinateResolver, renderer: Renderer) {
173-
// Draws arrow body.
174-
renderer.drawPlotLines(points: [start, end],
175-
strokeWidth: strokeWidth,
176-
strokeColor: color,
177-
isDashed: isDashed)
178-
179+
public func drawBody(renderer: Renderer) {
180+
switch headStyle {
181+
case .wedge:
182+
var p1 = start + Point(0.0, -strokeWidth/2)
183+
var p2 = start + Point(0.0, strokeWidth/2)
184+
let wedgeRotateAngle = -atan2(end.x - start.x, end.y - start.y)
185+
p1 = rotatePoint(point: p1, center: start, angleRadians: wedgeRotateAngle + 0.5 * Float.pi)
186+
p2 = rotatePoint(point: p2, center: start, angleRadians: wedgeRotateAngle + 0.5 * Float.pi)
187+
renderer.drawSolidPolygon(points: [p1, p2, end],
188+
fillColor: color)
189+
default:
190+
renderer.drawPlotLines(points: [start, end],
191+
strokeWidth: strokeWidth,
192+
strokeColor: color,
193+
isDashed: isDashed)
194+
}
195+
}
196+
public func drawHead(renderer: Renderer, a: Point, b: Point) {
179197
// Calculates arrow head points.
180-
var p1 = end + Point(cos(headAngle)*headLength, sin(headAngle)*headLength)
181-
var p2 = end + Point(cos(headAngle)*headLength, -sin(headAngle)*headLength)
182-
let rotateAngle = -atan2(start.x - end.x, start.y - end.y)
183-
p1 = rotatePoint(point: p1, center: end, angleRadians: rotateAngle + 0.5 * Float.pi)
184-
p2 = rotatePoint(point: p2, center: end, angleRadians: rotateAngle + 0.5 * Float.pi)
198+
var p1 = b + Point(cos(headAngle)*headLength, sin(headAngle)*headLength)
199+
var p2 = b + Point(cos(headAngle)*headLength, -sin(headAngle)*headLength)
200+
let rotateAngle = -atan2(a.x - b.x, a.y - b.y)
201+
p1 = rotatePoint(point: p1, center: b, angleRadians: rotateAngle + 0.5 * Float.pi)
202+
p2 = rotatePoint(point: p2, center: b, angleRadians: rotateAngle + 0.5 * Float.pi)
185203

186204
// Draws arrow head points.
187-
if isFilled {
188-
renderer.drawSolidPolygon(points: [p1, end, p2],
189-
fillColor: color)
205+
switch headStyle {
206+
case .skeletal:
207+
renderer.drawPlotLines(points: [p1, b, p2],
208+
strokeWidth: strokeWidth,
209+
strokeColor: color,
210+
isDashed: isDashed)
211+
case .filled:
212+
renderer.drawSolidPolygon(points: [p1, b, p2],
213+
fillColor: color)
214+
case .dart:
215+
var p3 = end + Point(-headLength/2, 0.0)
216+
p3 = rotatePoint(point: p3, center: b, angleRadians: rotateAngle + 0.5 * Float.pi)
217+
renderer.drawSolidPolygon(points: [p1, p3, p2, b],
218+
fillColor: color)
219+
default:
220+
break
190221
}
191-
else {
192-
renderer.drawPlotLines(points: [p1, end, p2],
193-
strokeWidth: strokeWidth,
194-
strokeColor: color,
195-
isDashed: false)
222+
}
223+
public mutating func draw(resolver: CoordinateResolver, renderer: Renderer) {
224+
// Draws arrow body.
225+
drawBody(renderer: renderer)
226+
227+
// Draws arrow head(s).
228+
drawHead(renderer: renderer, a: start, b: end)
229+
if isDoubleHeaded {
230+
drawHead(renderer: renderer, a: end, b: start)
196231
}
197232

198233
//Draws start and end annotations if specified.
@@ -223,15 +258,16 @@ struct Arrow : Annotation {
223258
endAnnotation?.draw(resolver: resolver, renderer: renderer)
224259
}
225260
}
226-
public init(color: Color = .black, start: Point = Point(0.0, 0.0), end: Point = Point(0.0, 0.0), strokeWidth: Float = 5, headLength: Float = 10, headAngle: Float = 20, isDashed: Bool = false, isFilled: Bool = false, startAnnotation: Annotation? = nil, endAnnotation: Annotation? = nil, overrideAnchor: Bool = false) {
261+
public init(color: Color = .black, start: Point = Point(0.0, 0.0), end: Point = Point(0.0, 0.0), strokeWidth: Float = 5, headLength: Float = 10, headAngle: Float = 20, isDashed: Bool = false, isDoubleHeaded: Bool = false, headStyle: HeadStyle = .skeletal, startAnnotation: Annotation? = nil, endAnnotation: Annotation? = nil, overrideAnchor: Bool = false) {
227262
self.color = color
228263
self.start = start
229264
self.end = end
230265
self.strokeWidth = strokeWidth
231266
self.headLength = headLength
232267
self.headAngle = headAngle * Float.pi / 180
233268
self.isDashed = isDashed
234-
self.isFilled = isFilled
269+
self.isDoubleHeaded = isDoubleHeaded
270+
self.headStyle = headStyle
235271
self.startAnnotation = startAnnotation
236272
self.endAnnotation = endAnnotation
237273
self.overrideAnchor = overrideAnchor
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
@testable import SwiftPlot
2+
import Foundation
3+
import SVGRenderer
4+
#if canImport(AGGRenderer)
5+
import AGGRenderer
6+
#endif
7+
#if canImport(QuartzRenderer)
8+
import QuartzRenderer
9+
#endif
10+
11+
@available(tvOS 13, watchOS 13, *)
12+
extension AnnotationTests {
13+
14+
func testAnnotationArrowDart() throws {
15+
16+
let fileName = "_32_arrow_annotation_dart"
17+
18+
var lineGraph = LineGraph<Float, Float>(enablePrimaryAxisGrid: true)
19+
lineGraph.addFunction(sin, minX: -5.0, maxX: 5.0, label: "sin(x)", color: .lightBlue)
20+
lineGraph.plotTitle.title = "FUNCTION"
21+
lineGraph.plotLabel.xLabel = "X-AXIS"
22+
lineGraph.plotLabel.yLabel = "Y-AXIS"
23+
lineGraph.plotLineThickness = 3.0
24+
25+
lineGraph.addAnnotation(annotation: Arrow(start: Point(400.0, 585.0),
26+
end: Point(585.0, 585.0),
27+
headLength: 20,
28+
headAngle: 30,
29+
headStyle: .dart,
30+
startAnnotation: Text(text: "relative maxima",
31+
direction: .west)))
32+
33+
let svg_renderer = SVGRenderer()
34+
try lineGraph.drawGraphAndOutput(fileName: svgOutputDirectory+fileName,
35+
renderer: svg_renderer)
36+
verifyImage(name: fileName, renderer: .svg)
37+
#if canImport(AGGRenderer)
38+
let agg_renderer = AGGRenderer()
39+
try lineGraph.drawGraphAndOutput(fileName: aggOutputDirectory+fileName,
40+
renderer: agg_renderer)
41+
verifyImage(name: fileName, renderer: .agg)
42+
#endif
43+
/*
44+
#if canImport(QuartzRenderer)
45+
let quartz_renderer = QuartzRenderer()
46+
try lineGraph.drawGraphAndOutput(fileName: coreGraphicsOutputDirectory+fileName,
47+
renderer: quartz_renderer)
48+
verifyImage(name: fileName, renderer: .coreGraphics)
49+
#endif
50+
*/
51+
}
52+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
@testable import SwiftPlot
2+
import Foundation
3+
import SVGRenderer
4+
#if canImport(AGGRenderer)
5+
import AGGRenderer
6+
#endif
7+
#if canImport(QuartzRenderer)
8+
import QuartzRenderer
9+
#endif
10+
11+
@available(tvOS 13, watchOS 13, *)
12+
extension AnnotationTests {
13+
14+
func testAnnotationArrowDoubleHeaded() throws {
15+
16+
let fileName = "_34_arrow_annotation_double_headed"
17+
18+
var lineGraph = LineGraph<Float, Float>(enablePrimaryAxisGrid: true)
19+
lineGraph.addFunction(sin, minX: -5.0, maxX: 5.0, label: "sin(x)", color: .lightBlue)
20+
lineGraph.plotTitle.title = "FUNCTION"
21+
lineGraph.plotLabel.xLabel = "X-AXIS"
22+
lineGraph.plotLabel.yLabel = "Y-AXIS"
23+
lineGraph.plotLineThickness = 3.0
24+
25+
lineGraph.addAnnotation(annotation: Arrow(start: Point(180.0, 585.0),
26+
end: Point(585.0, 585.0),
27+
isDoubleHeaded: true))
28+
29+
let svg_renderer = SVGRenderer()
30+
try lineGraph.drawGraphAndOutput(fileName: svgOutputDirectory+fileName,
31+
renderer: svg_renderer)
32+
verifyImage(name: fileName, renderer: .svg)
33+
#if canImport(AGGRenderer)
34+
let agg_renderer = AGGRenderer()
35+
try lineGraph.drawGraphAndOutput(fileName: aggOutputDirectory+fileName,
36+
renderer: agg_renderer)
37+
verifyImage(name: fileName, renderer: .agg)
38+
#endif
39+
/*
40+
#if canImport(QuartzRenderer)
41+
let quartz_renderer = QuartzRenderer()
42+
try lineGraph.drawGraphAndOutput(fileName: coreGraphicsOutputDirectory+fileName,
43+
renderer: quartz_renderer)
44+
verifyImage(name: fileName, renderer: .coreGraphics)
45+
#endif
46+
*/
47+
}
48+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
@testable import SwiftPlot
2+
import Foundation
3+
import SVGRenderer
4+
#if canImport(AGGRenderer)
5+
import AGGRenderer
6+
#endif
7+
#if canImport(QuartzRenderer)
8+
import QuartzRenderer
9+
#endif
10+
11+
@available(tvOS 13, watchOS 13, *)
12+
extension AnnotationTests {
13+
14+
func testAnnotationArrowWedge() throws {
15+
16+
let fileName = "_33_arrow_annotation_wedge"
17+
18+
var lineGraph = LineGraph<Float, Float>(enablePrimaryAxisGrid: true)
19+
lineGraph.addFunction(sin, minX: -5.0, maxX: 5.0, label: "sin(x)", color: .lightBlue)
20+
lineGraph.plotTitle.title = "FUNCTION"
21+
lineGraph.plotLabel.xLabel = "X-AXIS"
22+
lineGraph.plotLabel.yLabel = "Y-AXIS"
23+
lineGraph.plotLineThickness = 3.0
24+
25+
lineGraph.addAnnotation(annotation: Arrow(start: Point(500.0, 585.0),
26+
end: Point(585.0, 585.0),
27+
strokeWidth: 5,
28+
headStyle: .wedge,
29+
startAnnotation: Text(text: "relative maxima",
30+
direction: .west)))
31+
32+
let svg_renderer = SVGRenderer()
33+
try lineGraph.drawGraphAndOutput(fileName: svgOutputDirectory+fileName,
34+
renderer: svg_renderer)
35+
verifyImage(name: fileName, renderer: .svg)
36+
#if canImport(AGGRenderer)
37+
let agg_renderer = AGGRenderer()
38+
try lineGraph.drawGraphAndOutput(fileName: aggOutputDirectory+fileName,
39+
renderer: agg_renderer)
40+
verifyImage(name: fileName, renderer: .agg)
41+
#endif
42+
/*
43+
#if canImport(QuartzRenderer)
44+
let quartz_renderer = QuartzRenderer()
45+
try lineGraph.drawGraphAndOutput(fileName: coreGraphicsOutputDirectory+fileName,
46+
renderer: quartz_renderer)
47+
verifyImage(name: fileName, renderer: .coreGraphics)
48+
#endif
49+
*/
50+
}
51+
}
41.2 KB
Loading
40.7 KB
Loading
39.8 KB
Loading

0 commit comments

Comments
 (0)