Skip to content

Commit ce77ac3

Browse files
committed
More work on PlotElements. Still to do are:
- Horizontal/Vertical orientation for PlotElements (e.g. vertical labels) - Clean up new types that were introduced - (Probably later) convert markers to be PlotElements
1 parent 7db05ec commit ce77ac3

File tree

2 files changed

+150
-96
lines changed

2 files changed

+150
-96
lines changed

Sources/SwiftPlot/GraphLayout.swift

Lines changed: 144 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
1231
public 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

3249
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+
}
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+
///
3997
public 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,

Sources/SwiftPlot/Pair.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ extension Point {
2020
public func + (lhs: Point, rhs: Point) -> Point {
2121
return Point(lhs.x + rhs.x, lhs.y + rhs.y)
2222
}
23+
public func += (lhs: inout Point, rhs: Point) {
24+
lhs.x += rhs.x
25+
lhs.y += rhs.y
26+
}
2327

2428
public struct Size: Hashable {
2529
public var width: Float
@@ -90,7 +94,7 @@ extension Rect {
9094

9195
public init(size: Size, centeredOn center: Point) {
9296
self = Rect(
93-
origin: Point(center.x - size.width/2, center.y - size.height/2),
97+
origin: Point(center.x - (size.width/2), center.y - (size.height/2)),
9498
size: size
9599
)
96100
}
@@ -136,7 +140,7 @@ extension Rect {
136140
origin.x.round(.up)
137141
origin.y.round(.up)
138142
size.width.round(.down)
139-
size.width.round(.down)
143+
size.height.round(.down)
140144
}
141145

142146
// Subtraction operations.

0 commit comments

Comments
 (0)