-
Notifications
You must be signed in to change notification settings - Fork 39
Polygon and Polyline addition #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…oint` and adjust everywhere where it was being used.
…d 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.
…il` if `points` doesn't have enough points.
…ed to draw. There is no change in the output. This change is the same as the one made in PR KarthikRIyer#64.
… 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.
|
I need a bit more time to look at the implementation, but very much +1 to the idea! 👍 |
karwa
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm in favour of the idea of adding these geometry primitive types, but I think they should both just wrap an Array<Point> - as you can see, most of the time you're just extracting the first few elements from an existing array via a subscript and copying the rest in to the tail. We can just wrap that array directly and build the count-check in to the initialiser.
Sources/SwiftPlot/Renderer.swift
Outdated
|
|
||
| /// Polygon structure definition and sequence extension, along with its iterator. | ||
| public struct Polygon { | ||
| public var p1: Point, p2: Point, p3: Point |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this really necessary? We can also get a guarantee that there are at least 3 points with a single array and this initialiser:
public init?(points: [Point]) {
guard points.count >= 3 else { return nil }
self.init(points)
}
We could also add a precondition check on the polygon's didSet, checking that the number of values is not less than 3.
This would also make it trivial to turn a Polygon in to a Polyline (e.g. if you wanted to draw a stroke pattern on the polygon shape), since they would both just wrap Array<Point>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the polygon's didSet do you mean the wrapped array's didSet? The idea was to force the programmer to always give at least 2/3 points at the point of creation so no check is needed or optionally using the failable initializer where the check happens once at runtime, but I agree that this is probably too much for what is essentially a wrapper.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@karwa may I draw your attention to this? I will implement it as an array wrapper. Assuming you meant the didSet of the internal array of points, do we intend to allow people to just modify the internal array directly or should it be readonly? Maybe it makes sense to allow that, so these structs can start to be used earlier and points can be appended and such, but checking for the precondition for every single operation on the array may start hurting performance significantly eventually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure - I just mentioned the didSet because it is currently a public var, which allows mutations after the initialiser. Performance isn't really a massive concern - actually rendering the image will be the vast majority of the work. If it becomes a problem, we could make it @inlinable so the compiler can optimise those checks away. But really, only if it's a problem.
# Conflicts: # Sources/QuartzRenderer/QuartzRenderer.swift # Sources/SVGRenderer/SVGRenderer.swift # Sources/SwiftPlot/BarChart.swift # Sources/SwiftPlot/GraphLayout.swift # Sources/SwiftPlot/Histogram.swift # Sources/SwiftPlot/LineChart.swift # Sources/SwiftPlot/ScatterChart.swift # Tests/SwiftPlotTests/Reference/quartz/_01_single_series_line_chart.png # Tests/SwiftPlotTests/Reference/quartz/_02_multiple_series_line_chart.png # Tests/SwiftPlotTests/Reference/quartz/_03_sub_plot_horizontally_stacked_line_chart.png # Tests/SwiftPlotTests/Reference/quartz/_04_sub_plot_vertically_stacked_line_chart.png # Tests/SwiftPlotTests/Reference/quartz/_06_function_plot_line_chart.png # Tests/SwiftPlotTests/Reference/quartz/_07_secondary_axis_line_chart.png # Tests/SwiftPlotTests/Reference/quartz/_27_histogram_multi_stacked_step.png # Tests/SwiftPlotTests/Reference/quartz/_reg_57_histogram_stacked_step_offset.png
…econdition on the `didSet` one single failable initializer.
|
I've solved the merge conflicts and made the discussed changes. One last review and its good to merge! @karwa @KarthikRIyer |
karwa
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good - just a few minor issues.
- Correct precondition check - Remove label from `drawSolidPolygon` - Make a variadic version of the initialiser for `Polyline` and `Polygon`
|
I made the requested changes and I think everything is ready to merge! |
|
Looks good to me! 👍 |
I was thinking about what @karwa said in PR #64 regarding returning early instead of crashing in drawing methods of
Renderer:So I thought that instead of checking for the number of points passed to the
drawPlotLinesanddrawSolidPolygonmethods in each possible and future renderer, and hoping nobody forgets to do it, or doing the checks in earlier parts of the program (possibly leading to sprinkled and redundant.countchecks), we could maybe do something differently.The idea is to replace
[Point]in these methods with two new structs:PolylineandPolygon.Their purpose is to better utilize the type system so the information that these types must have at least 2 or 3 points can be propagated and the compiler helps us along the way. This pushes the responsibility of checking the amount of points as early as possible if it really is needed. If we don't check, we can't really construct any of these structs and ask the renderers to draw them.
In the case of the
drawDatamethod ofLineChartfor example, in order to ask to draw anything we must first construct aPolylineand for that we must give it at least 2 points. But in this case we could eventually pushPolylineup a level and make the code that generatesscaledValuestake care of checking that there are at least 2 values in there by usingPolylineas the type forscaledValues, and so on.I think this is a good approach taking leverage of Swift's type system to propagate information and the compiler to enforce we do the correct thing.
I'd like to hear your thoughts!
Ps: I also took the opportunity to change the
drawPlotLinesmethod on theQuartzRendererto draw the the lines correctly instead of callingdrawLinefor each pair of points.Edit: Correct mistakes and change order of Polyline and Polygon above to match "2 or 3 points" later on.