-
Notifications
You must be signed in to change notification settings - Fork 39
Add a heatmap #116
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
Add a heatmap #116
Conversation
|
Test failures are expected. The plots are now slightly larger because of changes to the way the area is calculated. |
|
I have to say I took a peek last year at your work on this in your fork and I got pretty excited about this! I think I like
What do you mean by this? Overall I really like it! 🎉 |
I also have a prototype of a bar-graph which is generic to any data. For example, if you have a collection of Removing the constraint that the data is a number and replacing it with an abstract way to get a number, we can allow the graphs to use higher-level data types. This has some really nice side-effects for things like data coordinates and formatting axes markers (because we're now a graph of In order to make this work, every plot that is generic like this will need an adapter. Because of that, I made an One downside of this generic-ness is that to do things like stacked datasets and multiple series, we need to create enormous chains of generic types. For example: StackedBarGraph<StackedBarGraph<StackedBarGraph<BarGraph<[Int]>, Data>, [Float]>, LazyMapSequence<(ClosedRange<Int>), MyStruct>>Nobody wants to write that, so it basically forces us to use the functional-style API. It's worth pointing about that SwiftUI also has this issue, but uses the non-standard "function builders" DSL instead. Maybe that would also be something for us one day.
Definitely. That will be source-breaking though, so maybe it's good if we tag a stable release before doing that.
This is about the heatmap's functional API. We have one for 2D collections, and one for any collection, which slices rows of a fixed width. This refers to adding another variant, which slices rows according to some line-breaking closure. I've since dropped this idea. It's good to keep the list of overloads down, so users are not overwhelmed. I'm not even 100% sure about the fixed-width slicing version; it's cool, but maybe it would be better as a general-purpose extension on |
|
I like I do like the functional API, but I think that there will be people who aren't comfortable with the functional approach(including me 😕 ). Is there a way to support both kinds of API?
|
|
Ohh... just saw the comments in the code. Would |
Sure. It would be more code, but I think it's possible. I'm not entirely sure how it would work with plots that compose multiple datasets, like One benefit of the initialiser approach is better integration with SwiftUI. SwiftUI has these protocols HStack {
Text("SwiftPlot meets SwiftUI").font(.headline)
Heatmap(values) {
$0.plotTitle.title = "\(values.count) values"
$0.colorMap = ColorMap.fiveColorHeatMap.lightened(by: 0.35)
$0.showGrid = true
}.frame(height: 600)
}.padding(10)
mapping makes a lot of sense, sure. |
That idea is interesting, but could you explain more about the use cases? Like I imagine it would be relatively easy to to inadvertently slice a collection where the resulting rows are of different lengths, what would happen then, can we still make a |
|
@odmir rows of different lengths are fine. The use-cases aren't that convincing though, and the standard library already provides this through a |
|
Ok, cool, lately I've been playing around with your branch because I had an idea I wanted to try out, so now I know in much more detail how the |
|
Ok so now I'd like to tell you about that idea I had. I thought the After having this in place we could create types conforming to these protocols that provide the functionality of "iterating" over 1D data or 2D data. One step further is to move the This approach would have several advantages:
Maybe there are other advantages I didn't think about... This opens up some really interesting possibilities and gives us a lot of flexibility:
If there is data that is very expensive to get or if we have a big tree of these types connected and the performance is starting to suffer, we could even do such things as create a conforming type that caches requested squares and that can be plugged anywhere that would make sense into this "tree". The most commonly used types or "trees" of types could be created by static methods on the data directly like in extensions to the Ok I'm sorry if you've gone through all that and got tired of all these shenanigans. I really think this approach opens up a lot of possibilities and I'd like to hear your opinions on this 😁. As I said before, I wanted to see how this would look like implemented, so I branched from the last commit in this draft and started implementing it just to get an idea. I checked and it draws the same graphs as the tests before, including the SVG's. If you'd like to see what it looks like, it is in my branch: odmir:heatmap_alternate. If you feel like following this approach, feel free to just copy it over to your branch! |
|
It's an interesting idea. The thing is, if we reduce the Heatmap down to the most basic things that it needs, it needs a 2D dataset. Even if it used a So it turns out these kinds of data transformations aren't really related to the Heatmap itself - they are generic Collection-y things. It's similar to the idea of the filter closure - in theory, you could use that to slice any kind of data in any way... and that's why it's in the standard library. I even think the fixed-width slicing could maybe be promoted to a generic collection algorithm. This is the thing - do we really want to implement all that logic again and again for every 2D plot? Not really. I'm not sure that the let hm = (-20..<20).lazy.map { x in
(-20..<20).lazy.map { y -> Int in
return (x * x) + (y * y)
}
}.plots.heatmap()
try renderAndVerify(hm)At the same time, I do think it's important that we use standard protocols like Plots are not usually drawn repeatedly without the data having changed. Even in SwiftUI, it only triggers when a dataset stored as a |
|
Thank you for opening my eyes... 😅 I was quite blinded thinking that the approach in this draft did not allow for the same things as my approach, I missed that it is trivial to make other types conform to I do think though we should eventually figure out a way to not do unnecessary work in layout every time we want to draw, for applications where the data never changes but the graph is resized. Anyways, I think this was a good exercise for me to tinker more with generics, PAT's and with general API design and underlying architecture. Thanks for the time and sorry if I made you waste a lot of time reading through my last comment and/or code in the branch! |
|
Designing API's and architecting code is really new to me and I can see that I really need more experience. Thank you for helping me through that, it has been a really interesting experience! |
|
By the way, do you think all the extensions on |
… to allow generating heatmaps of custom data-structures
… on Interpolator. This allows us to Heatmap sequences of arbitrary types (for example, using a KeyPath to an int or float to grade them). - Moved Interpolator to its own file. It should possibly be given a new name, too. - Added convenience functions for generating a Heatmap from arbitrary RACs or 2D Sequences. This allows for some really cool things.
…cense appears to allow - "derivative works" only need to mention how they changed matplotlib, which I did)
Added some geometry helpers for Rect.
…lts in a very tiny (like, sub-pixel) loss to accuracy, but results in a better image with sharper gridlines.
…iasing artefacts but shrinks the plot-size considerably. Next step will be, then, to allow data layout to change the plot-size. That's not going to be fun.
…n to determine the row locations, but doesn't require copying in to a RAC (which would also be O(n), *and* consume memory to boot). Allows direct heat-mapping of Strings (is that useful?). It would be nice if all charts did this and namespaced it under ".plots" or something (see ".lazy" in the Swift stdlib).
- Limit rounding on heatmap to elements with size > 1
…e helper functions on Color.
- 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.
- Clean up comments in GraphLayout/Heatmap
|
OK, so I've found the source of the weird rendering with the heatmap: sometimes AGG considers the markers to have size 9, and sometimes size 10. This is because some of them descend slightly below the baseline. I believe this doesn't affect the other examples because their markers happen to use longer strings. We should actually align all the markers by baseline, but we don't calculate or keep that information today. Doing that would improve the quality of our plots substantially, but it would be a lot of work and this patch is already big enough. That can be left as an improvement for the future - actually, Heatmap is doing nothing wrong. It just exposed a bug somewhere else. By the way, the So I think that's all the feedback addressed. @KarthikRIyer, anything to add? |
|
Nice, thank you for the format file as well! |
KarthikRIyer
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've started going through the changes and it'll take a little more time for me to complete that. The changes I've gone through till now look really good and interesting! There are probably some more things I mightn't understand because I'm just a basic swift user.
Also, how do you use the swift-format file? I've used this before https://github.com/apple/swift-format
var cgColor: CGColor {
if #available(OSX 10.15, iOS 13.0, *) {
return CGColor(
srgbRed: CGFloat(r),
green: CGFloat(g),
blue: CGFloat(b),
alpha: CGFloat(a))
} else {
var tuple = (CGFloat(r), CGFloat(g), CGFloat(b), CGFloat(a))
return withUnsafePointer(to: &tuple) { tupPtr in
return tupPtr.withMemoryRebound(to: CGFloat.self, capacity: 4) { floatPtr in
return CGColor(colorSpace: RGBColorSpace, components: floatPtr)!
}
}
}
}Why do we have two methods to get CGColor? |
Feel free to ask if there's anything that's unclear.
Yes, it's the same tool; the file is just stores the configuration for the project (so you can just run
There's only one computed property, but it branches depending on the SDK version, The first one (with SRGB values) is the only one available on some platforms (e.g. tvOS). But it's also quite new, so older platforms don't have it and have to use the colorspace/components initialiser as a fallback. |
@karwa I understood this but was wondering why we needed |
|
@KarthikRIyer The fallback initialiser requires a C-array of components (i.e. a pointer to the first element). See: https://developer.apple.com/documentation/coregraphics/cgcolor/1455927-init |
|
Ahh I see. I was thinking about https://developer.apple.com/documentation/coregraphics/cgcolor/1455631-init which is available 10.5+ but didn't notice that the other init was available 10.3+. |
|
Yes, but the main motivation is for iOS, tvOS, etc. The 10.5+ initialiser isn't available at all on, say, iOS 12. |
|
Understood @karwa . I went through the rest of the code and have got a gist of what's going on. Also do you think it's a good time to mark a stable release (after merging the bracket annotation of course). |
|
@KarthikRIyer yes, I think it would be a great idea to mark a stable release! There are some areas where we could make the API simpler to use or add features, but not in a compatible way. Having a stable version means existing code can keep working, even if later releases modify the API. For example, we use Similarly, |
|
Great! Let's release once @WilliamHYZhang's PR is merged. |
|
https://gist.github.com/KarthikRIyer/72074b291ccf83c07e0ce9579041e477 Here's a draft of the release notes for our next release. Could you'll review it? Also @karwa @boronhub @Qwerty71 could you please let me know your full names so that I can add them to the release notes? Also if it's fine with you could you share your twitter handles? I'd like to mention you'll in the post for the release. |
|
Also @karwa for the iOS/tvOS/watchOS support, would it be possible for you to provide an image with the plot on all three platforms? The current image just shows iOS and watchOS. |
|
The link seems to be broken on my end. |
|
Link updated @WilliamHYZhang. Is there a way to share draft releases? |
|
Thanks @KarthikRIyer, draft release looks awesome! |
|
Hi! The changelogs look brilliant! The project as a whole looks very promising! My name is Areeb Gani and my twitter is @areebg9. I'm also planning on finishing the polar plot and other github-related activities soon; I've been busy with many math competitions recently so I haven't found time to do much programming in the last 2 weeks. |
|
@KarthikRIyer this looks great, I'm really interested to see how this project develops! |
|
@KarthikRIyer My full name is Karl Wagner I'll make a image showing tvOS support later today. Also I could clean up and release a version of the AppKit/UIKit/SwiftUI bindings. I think it's better to keep those bindings separate for now, because they're still kind-of experimental (haven't been tested thoroughly) and there are a number of things I would like to change with the interface. |
|
This seems good. I could add a link to the bindings release in the release notes as well as the docs, for now. |
@karwa any updates on this? |
This still needs some cleaning-up. It introduces a lot of new things:
Heatmap<T>plot, which can plot any sequence of any type, by means of an adapter.Notice that the plot is created from the data (
data.plots.heatmap(....))AdjustsPlotSizeprotocol, which helps to reduce aliasing.PlotElements toGraphLayout(an object which gets laid-out and takes up space), and remodels axis labels to bePlotElements. This will allow us to support other items like color bars (which matplotlib can do)Some things I know I need to work on before this is ready to merge:
PlotElementis very descriptive. MaybeLayoutElement? Can anybody suggest a better name?Interpolator. PerhapsMappingorHeatmapAdapterwould be better? Note that users almost never need to write this name.Label, so we shouldn't need to convert them.heatmap.swift(e.g. axis labels)