@@ -9,15 +9,32 @@ public protocol PlotElement {
99 func measure( _ renderer: Renderer ) -> Size
1010 func draw( _ rect: Rect , renderer: Renderer )
1111}
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+
1231public struct Label : PlotElement {
1332 var text : String = " "
1433 var size : Float = 12
1534 var color : Color = . black
35+
1636 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)
2138 }
2239 public func draw( _ rect: Rect , renderer: Renderer ) {
2340 renderer. drawText ( text: text,
@@ -30,12 +47,53 @@ public struct Label: PlotElement {
3047}
3148
3249public 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+ }
3763}
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+
3889
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+ ///
3997public struct GraphLayout {
4098 // Inputs.
4199 var backgroundColor : Color = . white
@@ -118,16 +176,17 @@ extension GraphLayout {
118176 calculateMarkers: ( Size ) -> ( T , PlotMarkers ? , [ ( String , LegendIcon ) ] ? ) ) -> ( T , Results ) {
119177
120178 // 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)
123182
124183 // 2. Call back to the plot to lay out its data. It may ask to adjust the plot size.
125184 let ( drawingData, markers, legendInfo) = calculateMarkers ( plotSize)
126185 ( drawingData as? AdjustsPlotSize ) . map { plotSize = adjustPlotSize ( plotSize, info: $0) }
127186
128187 // 3. Now that we have the final sizes of everything, we can calculate their locations.
129188 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 )
131190 markers. map {
132191 var markers = $0
133192 roundMarkers ( & markers)
@@ -145,133 +204,124 @@ extension GraphLayout {
145204 static let yLabelPadding : Float = 10
146205 static let titleLabelPadding : Float = 14
147206
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 ] > {
151209 var elements = EdgeComponents < [ PlotElement ] > ( left: [ ] , top: [ ] , right: [ ] , bottom: [ ] )
152210 // TODO: Currently, only labels are "PlotElements".
153211 if !plotLabel. xLabel. isEmpty {
154212 let label = Label ( text: plotLabel. xLabel, size: plotLabel. size)
213+ . withPadding ( . all( Self . xLabelPadding) )
155214 elements. bottom. append ( label)
156- sizes. bottom. append ( label. measure ( renderer) )
157215 }
158216 if !plotLabel. yLabel. isEmpty {
159217 let label = Label ( text: plotLabel. yLabel, size: plotLabel. size)
218+ . withPadding ( . all( Self . yLabelPadding) )
160219 elements. left. append ( label)
161- sizes. left. append ( label. measure ( renderer) )
162220 }
163221 if !plotLabel. y2Label. isEmpty {
164222 let label = Label ( text: plotLabel. y2Label, size: plotLabel. size)
223+ . withPadding ( . all( Self . yLabelPadding) )
165224 elements. right. append ( label)
166- sizes. right. append ( label. measure ( renderer) )
167225 }
168226 if !plotTitle. title. isEmpty {
169227 let label = Label ( text: plotTitle. title, size: plotTitle. size)
228+ . withPadding ( . all( Self . titleLabelPadding) )
170229 elements. top. append ( label)
171- sizes. top. append ( label. measure ( renderer) )
172230 }
173- return ( sizes , elements)
231+ return elements
174232 }
175233
176234 /// 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+
191253 // 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
196260 }
197261
198262 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
199268
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)
220287
221288 // Elements are laid out so that [0] is closest to the plot.
222289 // Top elements.
223290 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) )
227294 results. rects. top. append ( rect)
228- renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
229295 t_height += itemSize. height
296+ renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
230297 }
231298 // 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) )
236303 results. rects. bottom. append ( rect)
237- renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
238304 b_height += itemSize. height
305+ renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
239306 }
240307 // 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) )
245312 results. rects. right. append ( rect)
246- renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
247313 r_width += itemSize. width
314+ renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
248315 }
249316 // 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) )
254321 results. rects. left. append ( rect)
255- renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
256322 l_width += itemSize. width
323+ renderer. drawSolidRect ( rect, fillColor: Color . random ( ) . withAlpha ( 1 ) , hatchPattern: . none)
257324 }
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
275325 }
276326
277327 /// Makes adjustments to the layout as requested by the plot.
@@ -518,7 +568,7 @@ extension GraphLayout {
518568 strokeColor: plotBorder. color,
519569 isDashed: false )
520570 renderer. drawText ( text: results. plotMarkers. y2MarkersText [ index] ,
521- location: results. y2MarkersTextLocation [ index] + rect. origin,
571+ location: results. y2MarkersTextLocation [ index] + rect. origin + Pair ( border , 0 ) ,
522572 textSize: markerTextSize,
523573 color: plotBorder. color,
524574 strokeWidth: 0.7 ,
0 commit comments