Skip to content

Commit 71f5e6c

Browse files
authored
feat: Polygon and Polyline addition (#67)
* rfac: Change global constant `zeroPoint` into a static property on `Point` and adjust everywhere where it was being used. * rfac: Change `drawSolidPolygon` to take a new struct `Polygon` instead of `[Point]`. - This allows us to be sure that, in each renderer, there will always be at least 3 points, avoiding having to check for that in every renderer. - This moves the problem of ensuring that there are always 3 points up the stack, making sure that the programmer ensures this as soon as possible by taking advantage of the type system, while letting the rest of the code not worry about this. * rfac: Reorder init. * docs: Fix documentation for `drawSolidPolygon` * bfix: Change `drawPlotLines` to draw a single polyline. * rfac: Don't be clever with iterators. * Add a failable initializer taking an array of points which returns `nil` if `points` doesn't have enough points. * rfac: Improve rules of `.step` so no lines with 0 or 1 points are asked to draw. There is no change in the output. This change is the same as the one made in PR #64. * rfac: `drawPlotLines` now takes a new struct `Polyline` instead of an array of points. This has the same advantage as the `Polygon` of making the user handle the fact that it needs at least 3 points, as early as possible and preserving that fact in the type itself. * chor: Rename `drawPlotLines` to `drawPolyline` * Change Polyline and Polygon to internally use just an array with a precondition on the `didSet` one single failable initializer. * Make requested changes and corrections from review - Correct precondition check - Remove label from `drawSolidPolygon` - Make a variadic version of the initialiser for `Polyline` and `Polygon`
2 parents 6d0d8bb + 4ae8103 commit 71f5e6c

40 files changed

+154
-103
lines changed

Sources/AGGRenderer/AGGRenderer/AGGRenderer.swift

+15-19
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import SwiftPlot
44

55
public class AGGRenderer: Renderer{
66

7-
public var offset = zeroPoint
7+
public var offset: Point = .zero
88
public var imageSize: Size {
99
willSet {
1010
delete_plot(agg_object);
@@ -156,19 +156,17 @@ public class AGGRenderer: Renderer{
156156
agg_object);
157157
}
158158

159-
public func drawSolidPolygon(points: [Point],
159+
public func drawSolidPolygon(_ polygon: SwiftPlot.Polygon,
160160
fillColor: Color) {
161-
precondition(points.count > 2, "drawSolidPolygon: Cannot draw a polygon with \(points.count) points.")
162-
163161
var x = [Float]()
164162
var y = [Float]()
165-
for index in 0..<points.count {
166-
x.append(points[index].x + xOffset)
167-
y.append(points[index].y + yOffset)
163+
for point in polygon.points {
164+
x.append(point.x + xOffset)
165+
y.append(point.y + yOffset)
168166
}
169167
draw_solid_polygon(x,
170168
y,
171-
Int32(points.count),
169+
Int32(x.count),
172170
fillColor.r,
173171
fillColor.g,
174172
fillColor.b,
@@ -200,23 +198,21 @@ public class AGGRenderer: Renderer{
200198
agg_object)
201199
}
202200

203-
public func drawPlotLines(points p: [Point],
204-
strokeWidth thickness: Float,
205-
strokeColor: Color,
206-
isDashed: Bool) {
207-
precondition(p.count > 1, "drawPlotLines: Cannot draw lines with \(p.count) points.")
208-
201+
public func drawPolyline(_ polyline: Polyline,
202+
strokeWidth thickness: Float,
203+
strokeColor: Color,
204+
isDashed: Bool) {
209205
var x = [Float]()
210206
var y = [Float]()
211-
212-
for index in 0..<p.count {
213-
x.append(p[index].x + xOffset)
214-
y.append(p[index].y + yOffset)
207+
208+
for point in polyline.points {
209+
x.append(point.x + xOffset)
210+
y.append(point.y + yOffset)
215211
}
216212

217213
draw_plot_lines(x,
218214
y,
219-
Int32(p.count),
215+
Int32(x.count),
220216
thickness,
221217
strokeColor.r,
222218
strokeColor.g,

Sources/QuartzRenderer/QuartzRenderer.swift

+17-17
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class QuartzRenderer: Renderer {
1919
/// Whether or not this context was given to us. If `true`, we should never re-make `context`
2020
let isExternalContext: Bool
2121
var fontPath = ""
22-
public var offset = zeroPoint
22+
public var offset: Point = .zero
2323

2424
public var fontSmoothing: Bool = false {
2525
didSet { context.setShouldSmoothFonts(fontSmoothing) }
@@ -381,15 +381,11 @@ public class QuartzRenderer: Renderer {
381381
context.fillPath()
382382
}
383383

384-
public func drawSolidPolygon(points: [Point],
384+
public func drawSolidPolygon(_ polygon: SwiftPlot.Polygon,
385385
fillColor: Color) {
386-
precondition(points.count > 2, "drawSolidPolygon: Cannot draw a polygon with \(points.count) points.")
387-
388386
let polygonPath = CGMutablePath()
389-
polygonPath.move(to: CGPoint(x: Double(points[0].x + xOffset), y: Double(points[0].y + yOffset)))
390-
for index in 1..<points.count {
391-
polygonPath.addLine(to: CGPoint(x: Double(points[index].x + xOffset), y: Double(points[index].y + yOffset)))
392-
}
387+
polygonPath.addLines(between: polygon.points.map { CGPoint(x: CGFloat($0.x), y: CGFloat($0.y)) },
388+
transform: CGAffineTransform(translationX: CGFloat(xOffset), y: CGFloat(yOffset)))
393389
polygonPath.closeSubpath()
394390
context.setFillColor(fillColor.cgColor)
395391
context.addPath(polygonPath)
@@ -415,19 +411,23 @@ public class QuartzRenderer: Renderer {
415411
context.setLineDash(phase: 1, lengths: [])
416412
}
417413

418-
public func drawPlotLines(points p: [Point],
414+
public func drawPolyline(_ polyline: Polyline,
419415
strokeWidth thickness: Float,
420416
strokeColor: Color,
421417
isDashed: Bool) {
422-
precondition(p.count > 1, "drawPlotLines: Cannot draw lines with \(p.count) points.")
423-
424-
for i in 0..<p.count-1 {
425-
drawLine(startPoint: p[i],
426-
endPoint: p[i+1],
427-
strokeWidth: thickness,
428-
strokeColor: strokeColor,
429-
isDashed: isDashed)
418+
419+
let linePath = CGMutablePath()
420+
linePath.addLines(between: polyline.points.map { CGPoint(x: CGFloat($0.x), y: CGFloat($0.y)) },
421+
transform: CGAffineTransform(translationX: CGFloat(xOffset), y: CGFloat(yOffset)))
422+
context.setStrokeColor(strokeColor.cgColor)
423+
context.setLineWidth(CGFloat(thickness))
424+
context.addPath(linePath)
425+
if(isDashed) {
426+
let dashes: [ CGFloat ] = [ CGFloat(thickness + 1), CGFloat(thickness + 1) ]
427+
context.setLineDash(phase: 1, lengths: dashes)
430428
}
429+
context.strokePath()
430+
context.setLineDash(phase: 1, lengths: [])
431431
}
432432

433433
public func drawText(text s: String,

Sources/SVGRenderer/SVGRenderer.swift

+11-15
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ public class SVGRenderer: Renderer{
2323
static let horizontalHatch: String = #"<defs><pattern id="horizontalHatch" width="10" height="10" patternUnits="userSpaceOnUse"><line x1="0" y1="5" x2="10" y2="5" style="stroke:black; stroke-width:1" /></pattern></defs>"#
2424
static let gridHatch: String = #"<defs><pattern id="gridHatch" width="10" height="10" patternUnits="userSpaceOnUse"><line x1="0" y1="5" x2="10" y2="5" style="stroke:black; stroke-width:1" /><line x1="5" y1="0" x2="5" y2="10" style="stroke:black; stroke-width:1" /></pattern></defs>"#
2525
static let crossHatch: String = #"<defs><pattern id="crossHatch" width="10" height="10" patternUnits="userSpaceOnUse"><line x1="0" y1="0" x2="10" y2="10" style="stroke:black; stroke-width:1" /><line x1="0" y1="10" x2="10" y2="0" style="stroke:black; stroke-width:1" /></pattern></defs>"#
26-
27-
public var offset = zeroPoint
26+
public var offset: Point = .zero
2827
public var imageSize: Size
2928

3029
var hatchingIncluded = Array(repeating: false,
@@ -165,18 +164,17 @@ public class SVGRenderer: Renderer{
165164
let triangle = #"<polygon points="\#(p1.x),\#(p1.y) \#(p2.x),\#(p2.y) \#(p3.x),\#(p3.y)" style="fill:\#(fillColor.svgColorString);opacity:\#(fillColor.a)" />"#
166165
lines.append(triangle)
167166
}
168-
169-
public func drawSolidPolygon(points: [Point],
167+
168+
public func drawSolidPolygon(_ polygon: SwiftPlot.Polygon,
170169
fillColor: Color) {
171-
precondition(points.count > 2, "drawSolidPolygon: Cannot draw a polygon with \(points.count) points.")
172-
173-
let pts = points.map { convertToSVGCoordinates($0) }
174170
var pointsString = ""
175-
for index in 0..<pts.count {
176-
pointsString = pointsString + "\(pts[index].x),\(pts[index].y) "
171+
for point in polygon.points {
172+
let convertedPoint = convertToSVGCoordinates(point)
173+
pointsString.append("\(convertedPoint.x),\(convertedPoint.y) ")
177174
}
178-
let polygon = #"<polygon points="\#(pointsString)" style="fill:\#(fillColor.svgColorString);opacity:\#(fillColor.a)" />"#
179-
lines.append(polygon)
175+
176+
let polygonString = #"<polygon points="\#(pointsString)" style="fill:\#(fillColor.svgColorString);opacity:\#(fillColor.a)" />"#
177+
lines.append(polygonString)
180178
}
181179

182180
public func drawLine(startPoint p1: Point,
@@ -196,13 +194,11 @@ public class SVGRenderer: Renderer{
196194
lines.append(line)
197195
}
198196

199-
public func drawPlotLines(points p: [Point],
197+
public func drawPolyline(_ polyline: Polyline,
200198
strokeWidth thickness: Float,
201199
strokeColor: Color,
202200
isDashed: Bool) {
203-
precondition(p.count > 1, "drawPlotLines: Cannot draw lines with \(p.count) points.")
204-
205-
let pointsString = p.lazy.map { point in
201+
let pointsString = polyline.points.lazy.map { point in
206202
let convertedPoint = self.convertToSVGCoordinates(point)
207203
return "\(convertedPoint.x),\(convertedPoint.y)"
208204
}.joined(separator: " ")

Sources/SwiftPlot/BarChart.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ extension BarGraph: HasGraphLayout {
9494
var stackSeries_scaledValues = [[Pair<Float,Float>]]()
9595
var scaleY: Float = 1
9696
var scaleX: Float = 1
97-
var barWidth : Int = 0
98-
var origin = zeroPoint
97+
var barWidth: Int = 0
98+
var origin: Point = .zero
9999
}
100100

101101
// functions implementing plotting logic
@@ -136,7 +136,7 @@ extension BarGraph: HasGraphLayout {
136136
}
137137

138138
if (minimumY >= U(0)) {
139-
results.origin = zeroPoint
139+
results.origin = .zero
140140
minimumY = U(0)
141141
}
142142
else{
@@ -226,7 +226,7 @@ extension BarGraph: HasGraphLayout {
226226
}
227227

228228
if minimumX >= U(0) {
229-
results.origin = zeroPoint
229+
results.origin = .zero
230230
minimumX = U(0)
231231
}
232232
else{

Sources/SwiftPlot/GraphLayout.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public struct GraphLayout {
119119
/// Calculates the region of the plot which is used for displaying the plot's data (inside all of the chrome).
120120
private func calcBorder(totalSize: Size, labelSizes: Results.LabelSizes, renderer: Renderer) -> Rect {
121121
var borderRect = Rect(
122-
origin: zeroPoint,
122+
origin: .zero,
123123
size: totalSize
124124
)
125125
if let xLabel = labelSizes.xLabelSize {
@@ -211,7 +211,7 @@ public struct GraphLayout {
211211
// Drawing.
212212

213213
func drawBackground(results: Results, renderer: Renderer) {
214-
renderer.drawSolidRect(Rect(origin: zeroPoint, size: results.totalSize),
214+
renderer.drawSolidRect(Rect(origin: .zero, size: results.totalSize),
215215
fillColor: backgroundColor, hatchPattern: .none)
216216
if let plotBackgroundColor = plotBackgroundColor {
217217
renderer.drawSolidRect(results.plotBorderRect, fillColor: plotBackgroundColor, hatchPattern: .none)

Sources/SwiftPlot/Histogram.swift

+6-4
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ extension Histogram: HasGraphLayout {
8181

8282
var barWidth: Float = 0
8383
let xMargin: Float = 5
84-
var origin = zeroPoint
84+
var origin: Point = .zero
8585
}
8686

8787
// functions implementing plotting logic
@@ -274,9 +274,11 @@ extension Histogram: HasGraphLayout {
274274
var frontLeftBinHeight = frontHeightsSlice.removeFirst()
275275
for ((backRightBinHeight, frontRightBinHeight), x) in zip(zip(backHeightsSlice, frontHeightsSlice), xValues) {
276276
func endLine() {
277-
renderer.drawPlotLines(points: line, strokeWidth: strokeWidth,
278-
strokeColor: allSeriesInfo[seriesIdx].color,
279-
isDashed: false)
277+
// This algorithm should never produce lines with less than 2 points
278+
guard let polyline = Polyline(line) else { fatalError("Histogram.drawData: Expecting 2 or more points, got \(line.count) instead.") }
279+
renderer.drawPolyline(polyline, strokeWidth: strokeWidth,
280+
strokeColor: allSeriesInfo[seriesIdx].color,
281+
isDashed: false)
280282
line.removeAll(keepingCapacity: true)
281283
}
282284

Sources/SwiftPlot/LineChart.swift

+15-9
Original file line numberDiff line numberDiff line change
@@ -154,19 +154,25 @@ extension LineGraph: HasGraphLayout {
154154
if let axisInfo = data.primaryAxisInfo {
155155
for dataset in primaryAxis.series {
156156
let points = dataset.values.map { axisInfo.convertCoordinate(fromData: $0) }
157-
renderer.drawPlotLines(points: points,
158-
strokeWidth: plotLineThickness,
159-
strokeColor: dataset.color,
160-
isDashed: false)
157+
guard let polyline = Polyline(points) else {
158+
fatalError("LineChart.drawData: Expecting 2 or more points, got \(points.count) instead")
159+
}
160+
renderer.drawPolyline(polyline,
161+
strokeWidth: plotLineThickness,
162+
strokeColor: dataset.color,
163+
isDashed: false)
161164
}
162165
}
163166
if let secondaryAxis = secondaryAxis, let axisInfo = data.secondaryAxisInfo {
164167
for dataset in secondaryAxis.series {
165168
let points = dataset.values.map { axisInfo.convertCoordinate(fromData: $0) }
166-
renderer.drawPlotLines(points: points,
167-
strokeWidth: plotLineThickness,
168-
strokeColor: dataset.color,
169-
isDashed: true)
169+
guard let polyline = Polyline(points) else {
170+
fatalError("LineChart.drawData: Expecting 2 or more points, got \(points.count) instead")
171+
}
172+
renderer.drawPolyline(polyline,
173+
strokeWidth: plotLineThickness,
174+
strokeColor: dataset.color,
175+
isDashed: true)
170176
}
171177
}
172178
}
@@ -184,7 +190,7 @@ extension LineGraph {
184190
var scaleY: Float = 1
185191
// The "origin" is just a known value at a known location,
186192
// used for calculating where other points are located.
187-
var origin: Point = zeroPoint
193+
var origin: Point = .zero
188194
var originValue = Pair(T(0), U(0))
189195

190196
init(series: [Series<T, U>], size: Size) {

Sources/SwiftPlot/Pair.swift

+5-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ public struct Pair<T,U> {
1010
}
1111

1212
public typealias Point = Pair<Float,Float>
13-
public let zeroPoint = Point(0.0, 0.0)
13+
14+
extension Point {
15+
public static let zero = Point(0.0, 0.0)
16+
}
1417

1518
public func + (lhs: Point, rhs: Point) -> Point {
1619
return Point(lhs.x + rhs.x, lhs.y + rhs.y)
@@ -42,7 +45,7 @@ public struct Rect {
4245
}
4346
extension Rect {
4447

45-
public static let empty = Rect(origin: zeroPoint, size: .zero)
48+
public static let empty = Rect(origin: .zero, size: .zero)
4649

4750
public var normalized: Rect {
4851
let normalizedOrigin = Point(origin.x + (size.width < 0 ? size.width : 0),

Sources/SwiftPlot/PlotStyleHelpers.swift

+15-11
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,14 @@ struct Arrow : Annotation {
184184
let wedgeRotateAngle = -atan2(end.x - start.x, end.y - start.y)
185185
p1 = rotatePoint(point: p1, center: start, angleRadians: wedgeRotateAngle + 0.5 * Float.pi)
186186
p2 = rotatePoint(point: p2, center: start, angleRadians: wedgeRotateAngle + 0.5 * Float.pi)
187-
renderer.drawSolidPolygon(points: [p1, p2, end],
187+
let head = Polygon(p1, p2, end)!
188+
renderer.drawSolidPolygon(head,
188189
fillColor: color)
189190
default:
190-
renderer.drawPlotLines(points: [start, end],
191-
strokeWidth: strokeWidth,
192-
strokeColor: color,
193-
isDashed: isDashed)
191+
renderer.drawPolyline(Polyline(start, end)!,
192+
strokeWidth: strokeWidth,
193+
strokeColor: color,
194+
isDashed: isDashed)
194195
}
195196
}
196197
public func drawHead(renderer: Renderer, a: Point, b: Point) {
@@ -204,17 +205,20 @@ struct Arrow : Annotation {
204205
// Draws arrow head points.
205206
switch headStyle {
206207
case .skeletal:
207-
renderer.drawPlotLines(points: [p1, b, p2],
208-
strokeWidth: strokeWidth,
209-
strokeColor: color,
210-
isDashed: isDashed)
208+
let head = Polyline(p1, b, p2)!
209+
renderer.drawPolyline(head,
210+
strokeWidth: strokeWidth,
211+
strokeColor: color,
212+
isDashed: isDashed)
211213
case .filled:
212-
renderer.drawSolidPolygon(points: [p1, b, p2],
214+
let head = Polygon(p1, b, p2)!
215+
renderer.drawSolidPolygon(head,
213216
fillColor: color)
214217
case .dart:
215218
var p3 = end + Point(-headLength/2, 0.0)
216219
p3 = rotatePoint(point: p3, center: b, angleRadians: rotateAngle + 0.5 * Float.pi)
217-
renderer.drawSolidPolygon(points: [p1, p3, p2, b],
220+
let head = Polygon(p1, p3, p2, b)!
221+
renderer.drawSolidPolygon(head,
218222
fillColor: color)
219223
default:
220224
break

0 commit comments

Comments
 (0)