Drawing Graphs in GH2
November 29, 2015
One of the more conspicuous ways in which working in Grasshopper differs from working in -say- Visual Studio, is that it is trivial to tweak values by hand at runtime. If one wishes to provide control over some constant used in a C# algorithm, one must either build a UI with buttons, sliders, checkboxes and whatnot, or maintain a live link to a settings file which can then be modified using notepad. The first approach requires a fair amount of work on the part of the developer, the latter is supremely user-unfriendly.
Although we collect no data on this, I would not be surprised in the least to find out that the most commonly used object in all Grasshopper files ever is the number slider. Numbers are almost always one of the required inputs of any given algorithm, and the slider provides an easy way to interactively adjust them. I do not think the implementation of sliders in Grasshopper 1.0 is perfect, but at least access to numeric constants works far better than the way in which GH1 provides access to equations. The Graph Mapper object is probably the most commonly used worst piece of UI garbage in the history of Grasshopper.
It doesn’t support panning or zooming, it doesn’t allow the positioning of grips outside of the domain, it doesn’t allow more than one graph at the same time, it doesn’t allow custom colours/thicknesses/dashpatterns, it doesn’t provide snapping or accurate positioning of grips; it drags the joy-of-use for the entire program down. Admittedly, there are lots of annoying buzzkills in Grasshopper, but the Graph Mapper has no excuse, there’s absolutely no reason for it to be crap, and yet here we are.
Since I’ve spend the last three years writing a lot of low-level code for Grasshopper 2.0 and I figured a good way to dip my toes back into the UI pool would be to try and design a better graph mapper.
This blog post is not about doing that. I’m only going to talk about drawing graph curves, which you may have thought was the one thing the old editor did well. Not, I’m ashamed to say, even that.
All the equation types that Grasshopper 1.0 provided (Bezier, Conic, Linear, Sinc, etc.) had three things in common:
- They could be evaluated at any location within the x-domain limits.
- They did not have discontinuities.
- They did not have sharp kinks (though Gaussian, Perlin, Sine and Sinc could be adjusted to at least have narrow spikes).
Let’s consider how breaking the above rules would affect the naïve graph drawing routine employed by Grasshopper 1.0, and then see if we can somehow improve it.
The most obvious way to draw the graph of a function is simply to evaluate that function at equally spaced intervals and then connect the dots. Drawing a polyline that connects a bunch of coordinates is trivial so the only thing we have to decide on, is the interval. Obviously, the closer together adjacent samples are, the more accurate our graph will be. On the other hand, lots of samples take lots of time to compute and we do not want the redraw frequency to drop significantly under 20~25 frames per second. Drawing several different functions into a full-screen window may well require tens of thousands of evaluations per draw event if we insist on evaluating everything at pixel accuracy. We will thus need a mechanism for throttling the sampling density, which in turn means we need to make our graphs look as good as possible with a very low number of samples.
As the above picture shows, sampling a function based on a repetitive grid can result in four major problems:
- If the function starts and stops in between samples, it will appear cut off. The visual error in this case is at most as severe as the sampling grid is coarse.
- If the function contains steep segments, they may be ‘missed’ by the sampling grid. This can potentially lead to a huge loss of visual detail.
- Functions with poles or asymptotes result in unconnected segments, but the sampling grid just steamrolls over this topological curiosity.
- Kinks that fall in between samples will appear blunted.
All of the above problems are the result of a sampling grid approach. If there’s a better way to approximate the actual function, then we should of course opt for that. Some functions consist solely of collections of straight line segments which can usually be converted into display geometry very efficiently and with a minimal loss of accuracy.
Take for example the block or nearest-neighbour interpolator. It contains only horizontal and vertical segments which are easy to compute. Throwing a sampling grid at such a graph will result in a jittery graph with slanted rather than vertical segments:
But we aren’t always so lucky as to be able to compute accurate geometry, and in many cases we do have to resort to sampling the function. Given an initial sampling grid, we may of course choose to insert additional samples at locations where we expect to be rewarded for our efforts. Ideally these locations are known in advance, but it could be we must postpone the insertion until after the initial sampling has taken place.
One place where it almost certainly pays to increase our density is between valid and invalid samples. We know for sure the function begins to exist somewhere between these two samples, but by looking more closely we can definitely narrow down the exact location. The cubic interpolator for example is only well defined between grips, not before the first or after the last one. It is after all an interpolator, not an extrapolator. This means the function only exists in the domain (Xa, Xb) where Xa is the lowest x-coordinate of all grips and Xb the highest. Using a naïve sampling grid which does not take the limits of the function into account, the drawn graph appears inconsistent even during panning.
The blunting of kinks is actually a very similar problem with a very similar solution. Instead of adding more samples to the edges of the function domain, we need to add samples to the interior where we expect to find kinks. Locations of grips are good places to expect a higher than average probability of kinks, and in any case we’d like the approximation of the graph to be as exact as possible at grip locations, lest they appear some distance away from the actual graph.
The log-linear interpolator is an example of a function with frequent kinks. Every time a grip is surrounded by grips that are significantly lower, a sharp kink will appear:
Finally (and most troublesome) a better way is needed to deal with poles. This is not just a matter of adjusting some sample locations, we must be able to draw the graph all the way to positive/negative infinity on either side of the pole, and we must take care to not connect the samples on either side, otherwise we give the impression the function actually exists where it in fact doesn’t.
Bulirsch-Stoer interpolation is a rational interpolation algorithm (as opposed to the more common polynomial algorithms) which has fallen out of favour since Michael Floater and Kai Hormann came up with an improved algorithm which doesn’t generate poles, but it does present a wonderful test case for graphing UI (both algorithms are available in Grasshopper 2.0 btw. along with Equidistant Polynomial, Neville Polynomial, Cubic, Akima, Log-Linear etc.). Behold the absolute foulness that is a function with poles imposed on a fixed sampling grid, as opposed to a grid that has been split into distinct sections with appropriate density distributions: