Skip to content

Commit 31b4ea9

Browse files
committed
GraphLayout Improvements:
- Added internal AdjustsPlotSize protocol for making slight changes to the plot's border rect. We need to think about this a bit more and how it affects the other plot chrome. - GraphLayout can now draw its grid as part of the foreground, *over* the data. - GraphLayout can now align marker labels between axis markers, instead of directly at them. - GraphLayout now draws its border correctly. Before, it would overlap the plot's area slightly and data at the edges could bleed in to the border. - Marker ticks and labels are now aware of the plot's border thickness. - Marker thickness can be set separately of the border thickness. For instance, you can still have markers if the border size is 0.
1 parent 3d8f95f commit 31b4ea9

File tree

1 file changed

+125
-50
lines changed

1 file changed

+125
-50
lines changed

Sources/SwiftPlot/GraphLayout.swift

Lines changed: 125 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,17 @@ public struct GraphLayout {
1818

1919
var enablePrimaryAxisGrid = true
2020
var enableSecondaryAxisGrid = true
21+
var drawsGridOverForeground = false
2122
var markerTextSize: Float = 12
23+
var markerThickness: Float = 2
2224
/// The amount of (horizontal) space to reserve for markers on the Y-axis.
2325
var yMarkerMaxWidth: Float = 40
26+
27+
enum MarkerLabelAlignment {
28+
case atMarker
29+
case betweenMarkers
30+
}
31+
var markerLabelAlignment = MarkerLabelAlignment.atMarker
2432

2533
struct Results : CoordinateResolver {
2634
/// The size these results have been calculated for; the entire size of the plot.
@@ -91,9 +99,11 @@ public struct GraphLayout {
9199
// 5. Round the markers to integer values.
92100
roundMarkers(&markers)
93101
results.plotMarkers = markers
94-
}
102+
}
95103
legendInfo.map { results.legendLabels = $0 }
96-
// 6. Lay out remaining chrome.
104+
// 6. Adjust the plot size if the graph requests less space.
105+
(drawingData as? AdjustsPlotSize).map { adjustPlotSize(info: $0, results: &results) }
106+
// 7. Lay out remaining chrome.
97107
calcMarkerTextLocations(renderer: renderer, results: &results)
98108
calcLegend(results.legendLabels, renderer: renderer, results: &results)
99109
return (drawingData, results)
@@ -142,23 +152,28 @@ public struct GraphLayout {
142152
// Add a space to the top when there is no title.
143153
borderRect.size.height -= Self.titleLabelPadding
144154
}
145-
borderRect.contract(by: plotBorder.thickness)
146155
// Give space for the markers.
147156
borderRect.clampingShift(dy: (2 * markerTextSize) + 10) // X markers
148157
// TODO: Better space calculation for Y/Y2 markers.
149158
borderRect.clampingShift(dx: yMarkerMaxWidth + 10) // Y markers
150159
borderRect.size.width -= yMarkerMaxWidth + 10 // Y2 markers
160+
// Space for border thickness.
161+
borderRect.contract(by: plotBorder.thickness)
151162

152163
// Sanitize the resulting rectangle.
153164
borderRect.size.width = max(borderRect.size.width, 0)
154165
borderRect.size.height = max(borderRect.size.height, 0)
155-
borderRect.origin.y.round(.up)
156-
borderRect.origin.x.round(.up)
157-
borderRect.size.width.round(.down)
158-
borderRect.size.height.round(.down)
166+
borderRect.roundInwards()
159167
return borderRect
160168
}
161169

170+
/// Makes adjustments to the layout as requested by the plot.
171+
private func adjustPlotSize(info: AdjustsPlotSize, results: inout Results) {
172+
if info.desiredPlotSize != .zero {
173+
results.plotBorderRect.size = info.desiredPlotSize
174+
}
175+
}
176+
162177
/// Rounds the given markers to integer pixel locations, for sharper gridlines.
163178
private func roundMarkers(_ markers: inout PlotMarkers) {
164179
for i in markers.xMarkers.indices {
@@ -196,20 +211,58 @@ public struct GraphLayout {
196211

197212
for i in 0..<results.plotMarkers.xMarkers.count {
198213
let textWidth = renderer.getTextWidth(text: results.plotMarkers.xMarkersText[i], textSize: markerTextSize)
199-
let text_p = Point(results.plotMarkers.xMarkers[i] - (textWidth/2), -2.0 * markerTextSize)
200-
results.xMarkersTextLocation.append(text_p)
214+
let markerLocation = results.plotMarkers.xMarkers[i]
215+
var textLocation = Point(0, -2.0 * markerTextSize)
216+
switch markerLabelAlignment {
217+
case .atMarker:
218+
textLocation.x = markerLocation - (textWidth/2)
219+
case .betweenMarkers:
220+
let nextMarkerLocation: Float
221+
if i < results.plotMarkers.xMarkers.endIndex - 1 {
222+
nextMarkerLocation = results.plotMarkers.xMarkers[i + 1]
223+
} else {
224+
nextMarkerLocation = results.plotBorderRect.width
225+
}
226+
let midpoint = markerLocation + (nextMarkerLocation - markerLocation)/2
227+
textLocation.x = midpoint - (textWidth/2)
228+
}
229+
results.xMarkersTextLocation.append(textLocation)
230+
}
231+
232+
/// Vertically aligns the label and returns the optimal Y coordinate to draw at.
233+
func alignYLabel(markers: [Float], index: Int, textSize: Size) -> Float {
234+
let markerLocation = markers[index]
235+
switch markerLabelAlignment {
236+
case .atMarker:
237+
return markerLocation - (textSize.height/2)
238+
case .betweenMarkers:
239+
let nextMarkerLocation: Float
240+
if index < markers.endIndex - 1 {
241+
nextMarkerLocation = markers[index + 1]
242+
} else {
243+
nextMarkerLocation = results.plotBorderRect.height
244+
}
245+
let midpoint = markerLocation + (nextMarkerLocation - markerLocation)/2
246+
return midpoint - (textSize.height/2)
247+
}
201248
}
202-
249+
203250
for i in 0..<results.plotMarkers.yMarkers.count {
204-
var textWidth = renderer.getTextWidth(text: results.plotMarkers.yMarkersText[i], textSize: markerTextSize)
205-
textWidth = min(textWidth, yMarkerMaxWidth)
206-
let text_p = Point(-textWidth - 8, results.plotMarkers.yMarkers[i] - 4)
207-
results.yMarkersTextLocation.append(text_p)
251+
var textSize = renderer.getTextLayoutSize(text: results.plotMarkers.yMarkersText[i],
252+
textSize: markerTextSize)
253+
textSize.width = min(textSize.width, yMarkerMaxWidth)
254+
var textLocation = Point(-textSize.width - 8, 0)
255+
textLocation.y = alignYLabel(markers: results.plotMarkers.yMarkers, index: i, textSize: textSize)
256+
results.yMarkersTextLocation.append(textLocation)
208257
}
209258

210259
for i in 0..<results.plotMarkers.y2Markers.count {
211-
let text_p = Point(results.plotBorderRect.width + 8, results.plotMarkers.y2Markers[i] - 4)
212-
results.y2MarkersTextLocation.append(text_p)
260+
var textSize = renderer.getTextLayoutSize(text: results.plotMarkers.y2MarkersText[i],
261+
textSize: markerTextSize)
262+
textSize.width = min(textSize.width, yMarkerMaxWidth)
263+
var textLocation = Point(results.plotBorderRect.width + 8, 0)
264+
textLocation.y = alignYLabel(markers: results.plotMarkers.y2Markers, index: i, textSize: textSize)
265+
results.y2MarkersTextLocation.append(textLocation)
213266
}
214267
}
215268

@@ -238,12 +291,17 @@ public struct GraphLayout {
238291
if let plotBackgroundColor = plotBackgroundColor {
239292
renderer.drawSolidRect(results.plotBorderRect, fillColor: plotBackgroundColor, hatchPattern: .none)
240293
}
241-
drawGrid(results: results, renderer: renderer)
294+
if !drawsGridOverForeground {
295+
drawGrid(results: results, renderer: renderer)
296+
}
242297
drawBorder(results: results, renderer: renderer)
243298
drawMarkers(results: results, renderer: renderer)
244299
}
245300

246301
func drawForeground(results: Results, renderer: Renderer) {
302+
if drawsGridOverForeground {
303+
drawGrid(results: results, renderer: renderer)
304+
}
247305
drawTitle(results: results, renderer: renderer)
248306
drawLabels(results: results, renderer: renderer)
249307
drawLegend(results.legendLabels, results: results, renderer: renderer)
@@ -289,7 +347,9 @@ public struct GraphLayout {
289347
}
290348

291349
private func drawBorder(results: Results, renderer: Renderer) {
292-
renderer.drawRect(results.plotBorderRect,
350+
var rect = results.plotBorderRect
351+
rect.contract(by: -plotBorder.thickness/2)
352+
renderer.drawRect(rect,
293353
strokeWidth: plotBorder.thickness,
294354
strokeColor: plotBorder.color)
295355
}
@@ -298,67 +358,74 @@ public struct GraphLayout {
298358
guard enablePrimaryAxisGrid || enablePrimaryAxisGrid else { return }
299359
let rect = results.plotBorderRect
300360
for index in 0..<results.plotMarkers.xMarkers.count {
301-
let p1 = Point(results.plotMarkers.xMarkers[index] + rect.minX, rect.minY)
302-
let p2 = Point(results.plotMarkers.xMarkers[index] + rect.minX, rect.maxY)
303-
renderer.drawLine(startPoint: p1,
304-
endPoint: p2,
305-
strokeWidth: grid.thickness,
306-
strokeColor: grid.color,
307-
isDashed: false)
361+
let p1 = Point(results.plotMarkers.xMarkers[index] + rect.minX, rect.minY)
362+
let p2 = Point(results.plotMarkers.xMarkers[index] + rect.minX, rect.maxY)
363+
guard rect.internalXCoordinates.contains(p1.x),
364+
rect.internalXCoordinates.contains(p2.x) else { continue }
365+
renderer.drawLine(startPoint: p1,
366+
endPoint: p2,
367+
strokeWidth: grid.thickness,
368+
strokeColor: grid.color,
369+
isDashed: false)
308370
}
309371

310372
if (enablePrimaryAxisGrid) {
311373
for index in 0..<results.plotMarkers.yMarkers.count {
312-
let p1 = Point(rect.minX, results.plotMarkers.yMarkers[index] + rect.minY)
313-
let p2 = Point(rect.maxX, results.plotMarkers.yMarkers[index] + rect.minY)
314-
renderer.drawLine(startPoint: p1,
315-
endPoint: p2,
316-
strokeWidth: grid.thickness,
317-
strokeColor: grid.color,
318-
isDashed: false)
374+
let p1 = Point(rect.minX, results.plotMarkers.yMarkers[index] + rect.minY)
375+
let p2 = Point(rect.maxX, results.plotMarkers.yMarkers[index] + rect.minY)
376+
guard rect.internalYCoordinates.contains(p1.y),
377+
rect.internalYCoordinates.contains(p2.y) else { continue }
378+
renderer.drawLine(startPoint: p1,
379+
endPoint: p2,
380+
strokeWidth: grid.thickness,
381+
strokeColor: grid.color,
382+
isDashed: false)
319383
}
320384
}
321385
if (enableSecondaryAxisGrid) {
322386
for index in 0..<results.plotMarkers.y2Markers.count {
323-
let p1 = Point(rect.minX, results.plotMarkers.y2Markers[index] + rect.minY)
324-
let p2 = Point(rect.maxX, results.plotMarkers.y2Markers[index] + rect.minY)
325-
renderer.drawLine(startPoint: p1,
326-
endPoint: p2,
327-
strokeWidth: grid.thickness,
328-
strokeColor: grid.color,
329-
isDashed: false)
387+
let p1 = Point(rect.minX, results.plotMarkers.y2Markers[index] + rect.minY)
388+
let p2 = Point(rect.maxX, results.plotMarkers.y2Markers[index] + rect.minY)
389+
guard rect.internalYCoordinates.contains(p1.y),
390+
rect.internalYCoordinates.contains(p2.y) else { continue }
391+
renderer.drawLine(startPoint: p1,
392+
endPoint: p2,
393+
strokeWidth: grid.thickness,
394+
strokeColor: grid.color,
395+
isDashed: false)
330396
}
331397
}
332398
}
333399

334400
private func drawMarkers(results: Results, renderer: Renderer) {
335401
let rect = results.plotBorderRect
402+
let border = plotBorder.thickness
336403
for index in 0..<results.plotMarkers.xMarkers.count {
337-
let p1 = Point(results.plotMarkers.xMarkers[index], -6) + rect.origin
338-
let p2 = Point(results.plotMarkers.xMarkers[index], 0) + rect.origin
404+
let p1 = Point(results.plotMarkers.xMarkers[index], -border - 6) + rect.origin
405+
let p2 = Point(results.plotMarkers.xMarkers[index], -border) + rect.origin
339406
renderer.drawLine(startPoint: p1,
340407
endPoint: p2,
341-
strokeWidth: plotBorder.thickness,
408+
strokeWidth: markerThickness,
342409
strokeColor: plotBorder.color,
343410
isDashed: false)
344411
renderer.drawText(text: results.plotMarkers.xMarkersText[index],
345-
location: results.xMarkersTextLocation[index] + rect.origin,
412+
location: results.xMarkersTextLocation[index] + rect.origin + Pair(0, -border),
346413
textSize: markerTextSize,
347414
color: plotBorder.color,
348415
strokeWidth: 0.7,
349416
angle: 0)
350417
}
351418

352419
for index in 0..<results.plotMarkers.yMarkers.count {
353-
let p1 = Point(-6, results.plotMarkers.yMarkers[index]) + rect.origin
354-
let p2 = Point(0, results.plotMarkers.yMarkers[index]) + rect.origin
420+
let p1 = Point(-border - 6, results.plotMarkers.yMarkers[index]) + rect.origin
421+
let p2 = Point(-border, results.plotMarkers.yMarkers[index]) + rect.origin
355422
renderer.drawLine(startPoint: p1,
356423
endPoint: p2,
357-
strokeWidth: plotBorder.thickness,
424+
strokeWidth: markerThickness,
358425
strokeColor: plotBorder.color,
359426
isDashed: false)
360427
renderer.drawText(text: results.plotMarkers.yMarkersText[index],
361-
location: results.yMarkersTextLocation[index] + rect.origin,
428+
location: results.yMarkersTextLocation[index] + rect.origin + Pair(-border, 0),
362429
textSize: markerTextSize,
363430
color: plotBorder.color,
364431
strokeWidth: 0.7,
@@ -367,13 +434,13 @@ public struct GraphLayout {
367434

368435
if !results.plotMarkers.y2Markers.isEmpty {
369436
for index in 0..<results.plotMarkers.y2Markers.count {
370-
let p1 = Point(results.plotBorderRect.width,
437+
let p1 = Point(results.plotBorderRect.width + border,
371438
(results.plotMarkers.y2Markers[index])) + rect.origin
372-
let p2 = Point(results.plotBorderRect.width + 6,
439+
let p2 = Point(results.plotBorderRect.width + border + 6,
373440
(results.plotMarkers.y2Markers[index])) + rect.origin
374441
renderer.drawLine(startPoint: p1,
375442
endPoint: p2,
376-
strokeWidth: plotBorder.thickness,
443+
strokeWidth: markerThickness,
377444
strokeColor: plotBorder.color,
378445
isDashed: false)
379446
renderer.drawText(text: results.plotMarkers.y2MarkersText[index],
@@ -427,6 +494,9 @@ public struct GraphLayout {
427494
}
428495
}
429496

497+
protocol AdjustsPlotSize {
498+
var desiredPlotSize: Size { get }
499+
}
430500
public protocol HasGraphLayout {
431501

432502
var layout: GraphLayout { get set }
@@ -500,6 +570,11 @@ extension HasGraphLayout {
500570
get { layout.markerTextSize }
501571
set { layout.markerTextSize = newValue }
502572
}
573+
574+
public var markerThickness: Float {
575+
get { layout.markerThickness }
576+
set { layout.markerThickness = newValue }
577+
}
503578
}
504579

505580
extension Plot where Self: HasGraphLayout {

0 commit comments

Comments
 (0)