Some time ago I wrote about plotting mathematical functions with Visiblox Charts, and Jesse responded by showing how to wrap a function in a data series to feed into a chart. I'm going to show how to take this idea a bit further, generating additional data on interaction, and allowing the user to specify a function to chart.
One limitation of the approach Jesse outlined is that the function range (and to a lesser extent its step) are explicitly specified. You can see that if you pan the chart, as this predefined limit is reached we go into uncharted territory (if you'll pardon the pun). What we'd really like to do is view any part of the function output - without generating more function outputs than required. There are two ways that spring to mind: firstly, bind the chart range to the function data range (which requires either making the data series a dependency object, or setting up the binding in reverse/via an intermediary); and secondly, via some magic which I will show below! My goal is to have a dataseries which will wrap a function (
double -> double for now) and chart the visible portion as appropriate, requiring no setup other than dropping it into the chart.
Basic Function Data Series
Let's start with a basic implementation of a DataSeries wrapping a function. I've simplified Jesse's version, but this is the same basic approach (also omitting niceties like argument validation).
This is nice and simple - perhaps too simple. Before going any further let's refine it a bit. While the minimum we have to do to construct a DataSeries is enumerate the points on demand, charting the series requires more than just looking at the data once, so it turns out this implementation is rather inefficient. Lets generate the data once, and instead present the data series as a list, allowing Visiblox to optimise.
Updating with the chart
This still doesn't use INotifyCollectionChanged, simply generating data on construction and adding it to the list. But data is only calculated once, points are only constructed once, and the data is available for random access as required. Now we're in a good place, so how do we generate more data dynamically when the chart range changes? By giving the data series a reference to the chart, or its axes, we can do this. In fact a reference to the IChartSeries used to display the IDataSeries gives exactly what we need: a reference to the axes used to display that series. From that, we can generate data according to the axis range - and when the range of those axes change, or the axes themselves are replaced, we can regenerate the data accordingly.
We update the data generation to notify changes:
(Note that I could have used ObservableCollection, but instead I implement INotifyCollectionChanged directly - we only ever replace all the data at once, so it's better to just fire a single Reset notification rather than updating each point in turn.)
I'll omit most of the event subscription/unsubscription boilerplate, but here's the meat of it:
I've regenerated the data only on actual range limits changing - this means that when zooming/panning when only the "effective" range changes, the data series does not regenerate. This might be a good idea for performance, but I mostly did this so that you can "see it working" - if you wanted to do this in reality, I'd recommend generating extra data off-screen to allow for a smoother panning experience.
Great! now we can tell our dataseries the IChartSeries used to render it, and it will update the generated data range accordingly. So:
And now for the sprinkle of magic. Visiblox defines an interface IChartSeriesAwareDataSeries. If a data series implements this interface - which consists of a single property
the data series is informed of the IChartSeries used to render it, when that is added to the chart. (Normally a data series is "just data", and may be displayed by multiple chart series - but in this case it only makes sense to consider this a 1:1 relationship). Then our example above becomes:
Plotting user functions
OK, so we have code to wrap a function in a data series. I'm going to demo that by charting a function typed in by the user. We'll chart functions of the form "y = f(x)", so lets just parse some simple arithmetic expressions of x.
For the parser, I've thrown together a parser in F# with FParsec, a parser combinator library. Now I wouldn't necessarily recommend introducing a dependency on the core F# DLLs and the FParsec library itself to parse a trivial language like this in an otherwise C# project. That said, it was fun to write and it's rather concise.
I won't ask you to understand the code in detail, but I'm reproducing it below to give a flavour of it. The constructed parser takes in a string, which is an expression of x, and (if successful) returns a function from x to the expression value (so
double->double). So there's no need to construct an Abstract Syntax Tree; instead the result of parsing a sub-expression is a function from x to the result of that expression, and these functions are combined to interpret larger expressions. (The funny symbols like
|>> are the FParsec's combinators, which allow parsers for subexpressions to be combined, with the different varieties for tasks like parsing 2 alternatives, or matching some text then passing it through a function)
Putting it all together:
(Try writing arithmetic expressions in x using *,/,+,-, sin(), cos() - note spaces are not permitted with this simple parser.)