Skip to content

Commit 1c9f624

Browse files
WilliamHYZhangKarthikRIyer
authored andcommitted
Add Anchor Support for Annotations (#100)
* add anchor support * bfix: remove quartz test * bfix: comment out quartz test * update anchor points, tests
1 parent 872f452 commit 1c9f624

File tree

5 files changed

+83
-10
lines changed

5 files changed

+83
-10
lines changed

Sources/SwiftPlot/PlotStyleHelpers.swift

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,29 +62,73 @@ public protocol Annotation {
6262
mutating func draw(resolver: CoordinateResolver, renderer: Renderer)
6363
}
6464

65-
struct Box: Annotation {
65+
public enum Direction {
66+
case north
67+
case east
68+
case south
69+
case west
70+
}
71+
72+
public protocol AnchorableAnnotation : Annotation {
73+
var direction: Direction { get set }
74+
var margin: Float { get set }
75+
mutating func resolve(renderer: Renderer, center: Point)
76+
}
77+
78+
struct Box: Annotation, AnchorableAnnotation {
6679
public var color = Color.black
6780
public var location = Point(0.0, 0.0)
6881
public var size = Size(width: 0.0, height: 0.0)
82+
public var direction = Direction.north
83+
public var margin: Float = 5
84+
public mutating func resolve(renderer: Renderer, center: Point) {
85+
switch(direction) {
86+
case .north:
87+
location = Point(center.x - size.width/2, center.y + margin)
88+
case .east:
89+
location = Point(center.x + margin, center.y - size.height/2)
90+
case .south:
91+
location = Point(center.x - size.width/2, center.y - size.height - margin)
92+
case .west:
93+
location = Point(center.x - size.width - margin, center.y - size.height/2)
94+
}
95+
}
6996
public func draw(resolver: CoordinateResolver, renderer: Renderer) {
7097
renderer.drawSolidRect(Rect(origin: location, size: size),
7198
fillColor: color,
7299
hatchPattern: .none)
73100
}
74-
public init(color: Color = .black, location: Point = Point(0.0, 0.0), size: Size = Size(width: 0.0, height: 0.0)) {
101+
public init(color: Color = .black, location: Point = Point(0.0, 0.0), size: Size = Size(width: 0.0, height: 0.0), direction: Direction = .north, margin: Float = 5) {
75102
self.color = color
76103
self.location = location
77104
self.size = size
105+
self.direction = direction
106+
self.margin = margin
78107
}
79108
}
80109

81-
struct Text : Annotation {
110+
struct Text : Annotation, AnchorableAnnotation {
82111
public var text = ""
83112
public var color = Color.black
84113
public var size: Float = 15
85114
public var location = Point(0.0, 0.0)
86115
public var boundingBox: Box?
87116
public var borderWidth: Float = 5
117+
public var direction = Direction.north
118+
public var margin: Float = 5
119+
public mutating func resolve(renderer: Renderer, center: Point) {
120+
let width = renderer.getTextWidth(text: text, textSize: size)
121+
switch(direction) {
122+
case .north:
123+
location = Point(center.x - width/2, center.y + margin)
124+
case .east:
125+
location = Point(center.x + margin, center.y - size/2)
126+
case .south:
127+
location = Point(center.x - width/2, center.y - size - margin)
128+
case .west:
129+
location = Point(center.x - width - margin, center.y - size/2)
130+
}
131+
}
88132
public mutating func draw(resolver: CoordinateResolver, renderer: Renderer) {
89133
if boundingBox != nil {
90134
var bboxSize = renderer.getTextLayoutSize(text: text, textSize: size)
@@ -101,13 +145,15 @@ struct Text : Annotation {
101145
strokeWidth: 1.2,
102146
angle: 0)
103147
}
104-
public init(text: String = "", color: Color = .black, size: Float = 15, location: Point = Point(0.0, 0.0), boundingBox: Box? = nil, borderWidth: Float = 5) {
148+
public init(text: String = "", color: Color = .black, size: Float = 15, location: Point = Point(0.0, 0.0), boundingBox: Box? = nil, borderWidth: Float = 5, direction: Direction = .north, margin: Float = 5) {
105149
self.text = text
106150
self.color = color
107151
self.size = size
108152
self.location = location
109153
self.boundingBox = boundingBox
110154
self.borderWidth = borderWidth
155+
self.direction = direction
156+
self.margin = margin
111157
}
112158
}
113159

@@ -122,6 +168,7 @@ struct Arrow : Annotation {
122168
public var isFilled: Bool = false
123169
public var startAnnotation: Annotation?
124170
public var endAnnotation: Annotation?
171+
public var overrideAnchor: Bool = false
125172
public mutating func draw(resolver: CoordinateResolver, renderer: Renderer) {
126173
// Draws arrow body.
127174
renderer.drawPlotLines(points: [start, end],
@@ -149,11 +196,34 @@ struct Arrow : Annotation {
149196
}
150197

151198
//Draws start and end annotations if specified.
152-
startAnnotation?.draw(resolver: resolver, renderer: renderer)
153-
endAnnotation?.draw(resolver: resolver, renderer: renderer)
154-
199+
if var startAnchor = startAnnotation as? AnchorableAnnotation {
200+
if !overrideAnchor {
201+
// Calculate anchor point
202+
var startAnchorPoint = start + Point(0.0, strokeWidth/2)
203+
let startAnchorRotateAngle = -atan2(end.x - start.x, end.y - start.y)
204+
startAnchorPoint = rotatePoint(point: startAnchorPoint, center: start, angleRadians: startAnchorRotateAngle + 0.5 * Float.pi)
205+
startAnchor.resolve(renderer: renderer, center: startAnchorPoint)
206+
}
207+
startAnchor.draw(resolver: resolver, renderer: renderer)
208+
}
209+
else {
210+
startAnnotation?.draw(resolver: resolver, renderer: renderer)
211+
}
212+
if var endAnchor = endAnnotation as? AnchorableAnnotation {
213+
if !overrideAnchor {
214+
// Calculate anchor point
215+
var endAnchorPoint = end + Point(0.0, strokeWidth/2)
216+
let endAnchorRotateAngle = -atan2(start.x - end.x, start.y - end.y)
217+
endAnchorPoint = rotatePoint(point: endAnchorPoint, center: end, angleRadians: endAnchorRotateAngle + 0.5 * Float.pi)
218+
endAnchor.resolve(renderer: renderer, center: endAnchorPoint)
219+
}
220+
endAnchor.draw(resolver: resolver, renderer: renderer)
221+
}
222+
else {
223+
endAnnotation?.draw(resolver: resolver, renderer: renderer)
224+
}
155225
}
156-
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) {
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) {
157227
self.color = color
158228
self.start = start
159229
self.end = end
@@ -164,5 +234,6 @@ struct Arrow : Annotation {
164234
self.isFilled = isFilled
165235
self.startAnnotation = startAnnotation
166236
self.endAnnotation = endAnnotation
237+
self.overrideAnchor = overrideAnchor
167238
}
168239
}

Tests/SwiftPlotTests/Annotation/annotation-arrow.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ extension AnnotationTests {
2525
lineGraph.addAnnotation(annotation: Arrow(start: Point(400.0, 585.0),
2626
end: Point(585.0, 585.0),
2727
startAnnotation: Text(text: "relative maxima",
28-
location: Point(280.0, 585.0))))
28+
direction: .west)))
2929

3030
let svg_renderer = SVGRenderer()
3131
try lineGraph.drawGraphAndOutput(fileName: svgOutputDirectory+fileName,
@@ -37,11 +37,13 @@ extension AnnotationTests {
3737
renderer: agg_renderer)
3838
verifyImage(name: fileName, renderer: .agg)
3939
#endif
40+
/*
4041
#if canImport(QuartzRenderer)
4142
let quartz_renderer = QuartzRenderer()
4243
try lineGraph.drawGraphAndOutput(fileName: coreGraphicsOutputDirectory+fileName,
4344
renderer: quartz_renderer)
4445
verifyImage(name: fileName, renderer: .coreGraphics)
4546
#endif
47+
*/
4648
}
4749
}
-26 Bytes
Loading
Binary file not shown.

Tests/SwiftPlotTests/Reference/svg/_31_arrow_annotation.svg

Lines changed: 1 addition & 1 deletion
Loading

0 commit comments

Comments
 (0)