@@ -9,15 +9,32 @@ public protocol PlotElement {
9
9
func measure( _ renderer: Renderer ) -> Size
10
10
func draw( _ rect: Rect , renderer: Renderer )
11
11
}
12
+ struct PaddedPlotElement < T: PlotElement > : PlotElement {
13
+ var base : T
14
+ var padding : EdgeComponents < Float > = . zero
15
+ func measure( _ renderer: Renderer ) -> Size {
16
+ var size = base. measure ( renderer)
17
+ size. width += padding. left + padding. right
18
+ size. height += padding. top + padding. bottom
19
+ return size
20
+ }
21
+ func draw( _ rect: Rect , renderer: Renderer ) {
22
+ base. draw ( rect. inset ( by: padding) , renderer: renderer)
23
+ }
24
+ }
25
+ extension PlotElement {
26
+ func withPadding( _ padding: EdgeComponents < Float > ) -> PlotElement {
27
+ return PaddedPlotElement ( base: self , padding: padding)
28
+ }
29
+ }
30
+
12
31
public struct Label : PlotElement {
13
32
var text : String = " "
14
33
var size : Float = 12
15
34
var color : Color = . black
35
+
16
36
public func measure( _ renderer: Renderer ) -> Size {
17
- var layoutSize = renderer. getTextLayoutSize ( text: text, textSize: size)
18
- layoutSize. width += 2 * GraphLayout. xLabelPadding
19
- layoutSize. height += 2 * GraphLayout. yLabelPadding
20
- return layoutSize
37
+ return renderer. getTextLayoutSize ( text: text, textSize: size)
21
38
}
22
39
public func draw( _ rect: Rect , renderer: Renderer ) {
23
40
renderer. drawText ( text: text,
@@ -30,12 +47,53 @@ public struct Label: PlotElement {
30
47
}
31
48
32
49
public struct EdgeComponents < T> {
33
- var left : T
34
- var top : T
35
- var right : T
36
- var bottom : T
50
+ public var left : T
51
+ public var top : T
52
+ public var right : T
53
+ public var bottom : T
54
+
55
+ static func all( _ value: T ) -> EdgeComponents < T > {
56
+ EdgeComponents ( left: value, top: value, right: value, bottom: value)
57
+ }
58
+
59
+ public func map< U> ( _ block: ( T ) throws -> U ) rethrows -> EdgeComponents < U > {
60
+ EdgeComponents < U > ( left: try block ( left) , top: try block ( top) ,
61
+ right: try block ( right) , bottom: try block ( bottom) )
62
+ }
37
63
}
64
+ extension EdgeComponents where T: ExpressibleByIntegerLiteral {
65
+ static var zero : Self { . all( 0 ) }
66
+ }
67
+ extension EdgeComponents where T: RangeReplaceableCollection {
68
+ static var empty : Self {
69
+ EdgeComponents ( left: . init( ) , top: . init( ) , right: . init( ) , bottom: . init( ) )
70
+ }
71
+ mutating func append< S> ( contentsOf other: EdgeComponents < S > ) where S: Sequence , S. Element == T . Element {
72
+ left. append ( contentsOf: other. left)
73
+ top. append ( contentsOf: other. top)
74
+ right. append ( contentsOf: other. right)
75
+ bottom. append ( contentsOf: other. bottom)
76
+ }
77
+ }
78
+ extension Rect {
79
+ func inset( by insets: EdgeComponents < Float > ) -> Rect {
80
+ var rect = self
81
+ rect. height -= insets. top + insets. bottom
82
+ rect. width -= insets. left + insets. right
83
+ rect. origin. x += insets. left
84
+ rect. origin. y += insets. bottom
85
+ return rect
86
+ }
87
+ }
88
+
38
89
90
+ /// A component for laying-out and rendering rectangular graphs.
91
+ ///
92
+ /// The principle 3 components of a `GraphLayout` are:
93
+ /// - The rectangular plot area itself,
94
+ /// - Any `PlotElement`s that surround the plot and take up space (e.g. the title, axis markers and labels), and
95
+ /// - Any `Annotation`s that are layered on top of the plot and do not take up space in a layout sense (e.g. arrows, watermarks).
96
+ ///
39
97
public struct GraphLayout {
40
98
// Inputs.
41
99
var backgroundColor : Color = . white
@@ -118,16 +176,17 @@ extension GraphLayout {
118
176
calculateMarkers: ( Size ) -> ( T , PlotMarkers ? , [ ( String , LegendIcon ) ] ? ) ) -> ( T , Results ) {
119
177
120
178
// 1. Calculate the plot size. To do that, we first have measure everything outside of the plot.
121
- let ( sizes, elements) = measureLabels ( renderer: renderer)
122
- var plotSize = calcBorder ( totalSize: size, labelSizes: sizes, renderer: renderer) . size
179
+ let elements = makePlotElements ( )
180
+ let sizes = elements. map { edgeElements in edgeElements. map { $0. measure ( renderer) } }
181
+ var plotSize = calcPlotSize ( totalSize: size, plotElements: sizes)
123
182
124
183
// 2. Call back to the plot to lay out its data. It may ask to adjust the plot size.
125
184
let ( drawingData, markers, legendInfo) = calculateMarkers ( plotSize)
126
185
( drawingData as? AdjustsPlotSize ) . map { plotSize = adjustPlotSize ( plotSize, info: $0) }
127
186
128
187
// 3. Now that we have the final sizes of everything, we can calculate their locations.
129
188
var results = Results ( totalSize: size, plotBorderRect: Rect ( origin: . zero, size: plotSize) ,
130
- elements: elements, sizes: sizes, rects: . init ( left : [ ] , top : [ ] , right : [ ] , bottom : [ ] ) )
189
+ elements: elements, sizes: sizes, rects: . empty )
131
190
markers. map {
132
191
var markers = $0
133
192
roundMarkers ( & markers)
@@ -145,133 +204,124 @@ extension GraphLayout {
145
204
static let yLabelPadding : Float = 10
146
205
static let titleLabelPadding : Float = 14
147
206
148
- /// Measures the sizes of chrome elements outside the plot's borders (axis titles, plot title, etc).
149
- private func measureLabels( renderer: Renderer ) -> ( EdgeComponents < [ Size ] > , EdgeComponents < [ PlotElement ] > ) {
150
- var sizes = EdgeComponents < [ Size ] > ( left: [ ] , top: [ ] , right: [ ] , bottom: [ ] )
207
+ // FIXME: To be removed. These items should already be PlotElements.
208
+ private func makePlotElements( ) -> EdgeComponents < [ PlotElement ] > {
151
209
var elements = EdgeComponents < [ PlotElement ] > ( left: [ ] , top: [ ] , right: [ ] , bottom: [ ] )
152
210
// TODO: Currently, only labels are "PlotElements".
153
211
if !plotLabel. xLabel. isEmpty {
154
212
let label = Label ( text: plotLabel. xLabel, size: plotLabel. size)
213
+ . withPadding ( . all( Self . xLabelPadding) )
155
214
elements. bottom. append ( label)
156
- sizes. bottom. append ( label. measure ( renderer) )
157
215
}
158
216
if !plotLabel. yLabel. isEmpty {
159
217
let label = Label ( text: plotLabel. yLabel, size: plotLabel. size)
218
+ . withPadding ( . all( Self . yLabelPadding) )
160
219
elements. left. append ( label)
161
- sizes. left. append ( label. measure ( renderer) )
162
220
}
163
221
if !plotLabel. y2Label. isEmpty {
164
222
let label = Label ( text: plotLabel. y2Label, size: plotLabel. size)
223
+ . withPadding ( . all( Self . yLabelPadding) )
165
224
elements. right. append ( label)
166
- sizes. right. append ( label. measure ( renderer) )
167
225
}
168
226
if !plotTitle. title. isEmpty {
169
227
let label = Label ( text: plotTitle. title, size: plotTitle. size)
228
+ . withPadding ( . all( Self . titleLabelPadding) )
170
229
elements. top. append ( label)
171
- sizes. top. append ( label. measure ( renderer) )
172
230
}
173
- return ( sizes , elements)
231
+ return elements
174
232
}
175
233
176
234
/// Calculates the region of the plot which is used for displaying the plot's data (inside all of the chrome).
177
- private func calcBorder( totalSize: Size , labelSizes: EdgeComponents < [ Size ] > , renderer: Renderer ) -> Rect {
178
- var borderRect = Rect ( origin: . zero, size: totalSize)
179
- labelSizes. left. forEach { borderRect. size. width -= $0. width }
180
- labelSizes. right. forEach { borderRect. size. width -= $0. width }
181
- labelSizes. top. forEach { borderRect. size. height -= $0. height }
182
- labelSizes. bottom. forEach { borderRect. size. height -= $0. height }
183
- // Give space for the markers.
184
- borderRect. clampingShift ( dy: ( 2 * markerTextSize) + 10 ) // X markers
185
- // TODO: Better space calculation for Y/Y2 markers.
186
- borderRect. clampingShift ( dx: yMarkerMaxWidth + 10 ) // Y markers
187
- borderRect. size. width -= yMarkerMaxWidth + 10 // Y2 markers
188
- // Space for border thickness.
189
- borderRect. contract ( by: plotBorder. thickness)
190
-
235
+ private func calcPlotSize( totalSize: Size , plotElements: EdgeComponents < [ Size ] > ) -> Size {
236
+ var plotSize = totalSize
237
+
238
+ // Subtract space for the plot elements.
239
+ plotElements. left. forEach { plotSize. width -= $0. width }
240
+ plotElements. right. forEach { plotSize. width -= $0. width }
241
+ plotElements. top. forEach { plotSize. height -= $0. height }
242
+ plotElements. bottom. forEach { plotSize. height -= $0. height }
243
+
244
+ // Subtract space for the markers.
245
+ // TODO: Make this more accurate.
246
+ plotSize. height -= ( 2 * markerTextSize) + 10 // X markers
247
+ plotSize. width -= yMarkerMaxWidth + 10 // Y markers
248
+ plotSize. width -= yMarkerMaxWidth + 10 // Y2 markers
249
+ // Subtract space for border thickness.
250
+ plotSize. height -= 2 * plotBorder. thickness
251
+ plotSize. width -= 2 * plotBorder. thickness
252
+
191
253
// Sanitize the resulting rectangle.
192
- borderRect. size. width = max ( borderRect. size. width, 0 )
193
- borderRect. size. height = max ( borderRect. size. height, 0 )
194
- borderRect. roundInwards ( )
195
- return borderRect
254
+ plotSize. height = max ( plotSize. height, 0 )
255
+ plotSize. width = max ( plotSize. width, 0 )
256
+ plotSize. height. round ( . down)
257
+ plotSize. width. round ( . down)
258
+
259
+ return plotSize
196
260
}
197
261
198
262
private func layoutObjects( _ renderer: Renderer , _ results: inout Results ) {
263
+ renderer. drawSolidRect ( . init( origin: . zero, size: results. totalSize) ,
264
+ fillColor: Color . random ( ) . withAlpha ( 0.5 ) , hatchPattern: . none)
265
+ // 1. Calculate the plotBorderRect.
266
+ // We already have the size, so we only need to calculate the origin.
267
+ var plotOrigin = Point . zero
199
268
200
- /*
201
- - First, calculate the total size taken on each side by PlotElements.
202
- - Also, add markers (TODO: Make markers in to PlotElements).
203
- - The remainder is available for the border rect (size should be >= to the results.plotBorderRect).
204
- - BUT: what if the plot wanted a smaller size?
205
- - In that case, we should center the plotBorderRect within that space.
206
- */
207
- renderer. drawSolidRect ( . init( origin: . zero, size: results. totalSize) , fillColor: Color . random ( ) . withAlpha ( 0.5 ) , hatchPattern: . none)
208
-
209
- // Adjust the plotBorderRect, in case its size has changed.
210
- var borderRect = Rect ( size: results. plotBorderRect. size,
211
- centeredOn: results. plotBorderRect. center)
212
- borderRect. origin. x += results. sizes. left. reduce ( into: 0 ) { $0 += $1. width }
213
- borderRect. origin. y += results. sizes. bottom. reduce ( into: 0 ) { $0 += $1. height }
214
-
215
- // Adjust for markers (TODO: they are not PlotElements yet).
216
- // Give space for the markers.
217
- borderRect. origin. y += ( 2 * markerTextSize) + 10 // X markers
218
- // TODO: Better space calculation for Y/Y2 markers.
219
- borderRect. origin. x += yMarkerMaxWidth + 10 // Y markers
269
+ // Offset by the left/bottom PlotElements.
270
+ plotOrigin. x += results. sizes. left. reduce ( into: 0 ) { $0 += $1. width }
271
+ plotOrigin. y += results. sizes. bottom. reduce ( into: 0 ) { $0 += $1. height }
272
+ // Offset by marker sizes (TODO: they are not PlotElements yet, so not handled above).
273
+ let xMarkerHeight = ( 2 * markerTextSize) + 10 // X markers
274
+ let yMarkerWidth = yMarkerMaxWidth + 10 // Y markers
275
+ plotOrigin. y += xMarkerHeight
276
+ plotOrigin. x += yMarkerWidth
277
+ // Offset by plot thickness.
278
+ plotOrigin. x += plotBorder. thickness
279
+ plotOrigin. y += plotBorder. thickness
280
+
281
+ // These are the final coordinates of the plot's internal space, so update `results`.
282
+ results. plotBorderRect = Rect ( origin: plotOrigin, size: results. plotBorderRect. size)
283
+
284
+ // 2. Lay out the PlotElements.
285
+ var plotExternalRect = results. plotBorderRect
286
+ plotExternalRect. contract ( by: - 1 * plotBorder. thickness)
220
287
221
288
// Elements are laid out so that [0] is closest to the plot.
222
289
// Top elements.
223
290
var t_height : Float = 0
224
- for (idx , itemSize) in results. sizes. top. enumerated ( ) {
225
- let rect = Rect ( origin: Point ( borderRect . minX, borderRect . maxY - t_height) ,
226
- size: Size ( width: borderRect . width, height: itemSize. height) )
291
+ for itemSize in results. sizes. top {
292
+ let rect = Rect ( origin: Point ( plotExternalRect . minX, plotExternalRect . maxY + t_height) ,
293
+ size: Size ( width: plotExternalRect . width, height: itemSize. height) )
227
294
results. rects. top. append ( rect)
228
- renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
229
295
t_height += itemSize. height
296
+ renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
230
297
}
231
298
// Bottom elements.
232
- var b_height : Float = 0
233
- for (idx , itemSize) in results. sizes. bottom. enumerated ( ) {
234
- let rect = Rect ( origin: Point ( borderRect . minX, borderRect . minY - b_height) ,
235
- size: Size ( width: borderRect . width, height: itemSize. height) )
299
+ var b_height : Float = xMarkerHeight
300
+ for itemSize in results. sizes. bottom {
301
+ let rect = Rect ( origin: Point ( plotExternalRect . minX, plotExternalRect . minY - b_height - itemSize . height ) ,
302
+ size: Size ( width: plotExternalRect . width, height: itemSize. height) )
236
303
results. rects. bottom. append ( rect)
237
- renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
238
304
b_height += itemSize. height
305
+ renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
239
306
}
240
307
// Right elements.
241
- var r_width : Float = 0
242
- for (idx , itemSize) in results. sizes. right. enumerated ( ) {
243
- let rect = Rect ( origin: Point ( borderRect . maxX + r_width, borderRect . minY) ,
244
- size: Size ( width: itemSize. width, height: borderRect . height) )
308
+ var r_width : Float = results . plotMarkers . y2Markers . isEmpty ? 0 : yMarkerWidth
309
+ for itemSize in results. sizes. right {
310
+ let rect = Rect ( origin: Point ( plotExternalRect . maxX + r_width, plotExternalRect . minY) ,
311
+ size: Size ( width: itemSize. width, height: plotExternalRect . height) )
245
312
results. rects. right. append ( rect)
246
- renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
247
313
r_width += itemSize. width
314
+ renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
248
315
}
249
316
// Left elements.
250
- var l_width : Float = 0
251
- for (idx , itemSize) in results. sizes. left. enumerated ( ) {
252
- let rect = Rect ( origin: Point ( borderRect . minX - l_width - itemSize. width, borderRect . minY) ,
253
- size: Size ( width: itemSize. width, height: borderRect . height) )
317
+ var l_width : Float = yMarkerWidth
318
+ for itemSize in results. sizes. left {
319
+ let rect = Rect ( origin: Point ( plotExternalRect . minX - l_width - itemSize. width, plotExternalRect . minY) ,
320
+ size: Size ( width: itemSize. width, height: plotExternalRect . height) )
254
321
results. rects. left. append ( rect)
255
- renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
256
322
l_width += itemSize. width
323
+ renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
257
324
}
258
-
259
-
260
-
261
- // if let titleLabel = labelSizes.titleSize {
262
- // borderRect.size.height -= (titleLabel.height + 2 * Self.titleLabelPadding)
263
- // } else {
264
- // // Add a space to the top when there is no title.
265
- // borderRect.size.height -= Self.titleLabelPadding
266
- // }
267
- // if let yLabel = labelSizes.yLabelSize {
268
- // borderRect.clampingShift(dx: yLabel.height + 2 * Self.yLabelPadding)
269
- // }
270
- // if let y2Label = labelSizes.y2LabelSize {
271
- // borderRect.size.width -= (y2Label.height + 2 * Self.yLabelPadding)
272
- // }
273
-
274
- results. plotBorderRect = borderRect
275
325
}
276
326
277
327
/// Makes adjustments to the layout as requested by the plot.
@@ -518,7 +568,7 @@ extension GraphLayout {
518
568
strokeColor: plotBorder. color,
519
569
isDashed: false )
520
570
renderer. drawText ( text: results. plotMarkers. y2MarkersText [ index] ,
521
- location: results. y2MarkersTextLocation [ index] + rect. origin,
571
+ location: results. y2MarkersTextLocation [ index] + rect. origin + Pair ( border , 0 ) ,
522
572
textSize: markerTextSize,
523
573
color: plotBorder. color,
524
574
strokeWidth: 0.7 ,
0 commit comments