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).

public class FunctionDataSeries : IDataSeries
{
    private Func<double, double> _function;
    private double _min, _max;

    public string Title { get; set; }

    public FunctionDataSeries(Func<double, double> function, double min, double max)
    {
        _function = function;
        _min = min;
        _max = max;
    }

    /// <summary>
    /// Simplest IDataSeries implementation - directly enumerate the data when asked for it.
    /// </summary>
    public System.Collections.IEnumerator GetEnumerator()
    {
        double step = Math.Max(0.0001, (_max - _min) / 100);
        for (double x = _min; x <= _max; x += step)
        {
            yield return new DataPoint<double, double>(x, _function(x));
        }
    }

    /// <summary>
    /// Trivial INotifyCollectionChanged implementation - do nothing as we don't change.
    /// </summary>
    public event NotifyCollectionChangedEventHandler CollectionChanged;
}

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.

public class FunctionDataSeries : List<IDataPoint>, IDataSeries
{
    private Func<double, double> _function;
    private double _min, _max;

    public string Title { get; set; }

    public FunctionDataSeries(Func<double, double> function, double min, double max)
    {
        _function = function;
        _min = min;
        _max = max;
        GenerateData();
    }

    private void GenerateData()
    {
        double step = Math.Max(0.0001, (_max - _min) / 100);
        for (double x = _min; x <= _max; x += step)
        {
            this.Add(new DataPoint<double, double>(x, _function(x)));
        }
    }

    public event NotifyCollectionChangedEventHandler CollectionChanged;
}

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:

private void GenerateData()
{
    this.Clear();
    double step = Math.Max(0.0001, (_max - _min) / 100);
    for (double x = _min; x <= _max; x += step)
    {
        this.Add(new DataPoint<double, double>(x, _function(x)));
    }
    OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}

protected void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
    if (CollectionChanged != null)
    {
        CollectionChanged(this, args);
    }
}

(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:

// Property with some event subscribe/unsubscribe logic using SubscribeAxis
public IChartSeries ChartSeries
{
	get { return _chartSeries; }
	set
        {
            // ...
            _chartSeries = value;
            // ...
            SubscribeAxis(_chartSeries.XAxis);
            // ...
        }
}
// ...
// Main event subscription logic using AxisEventRelay
private void SubscribeAxis(IAxis axis)
{
    // AxisEventRelay is a great way to be notified of maximum/minimum effective or actual range limit changes
    // without listening to properties directly.
    // Try changing this to AxisEventEnumeration.ActualRangeEffectiveLimitsChanged
    AxisEventRelay relay = new AxisEventRelay(axis, AxisEventEnumeration.ActualRangeLimitsChanged);
    relay.AxisEvent += Relay_AxisEvent;
    GenerateData();
}

private void Relay_AxisEvent(object sender, AxisEventRelayEventArgs e)
{
    var xAxis = _xAxisRelay.Axis;
    _min = (double)xAxis.ActualRange.Minimum;
    _max = (double)xAxis.ActualRange.Maximum;

    GenerateData();
}

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:

var dataSeries = new FunctionDataSeries(x => 2*x);
var chartSeries = new LineSeries { DataSeries = dataSeries };
dataSeries.ChartSeries = chartSeries;
myChart.Series.Add(chartSeries);

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

IChartSeries ChartSeries { get; set; }

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:

myChart.Series.Add(new LineSeries { DataSeries = new FunctionDataSeries(x => 2*x) } );

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 >>% and |>> 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)

let sym = pstring
(* environment is just a single double (the x value) *)
type env = double

(* variable becomes function returning the 'environment' x *)
let var = sym "x" >>% id

(* float literal becomes function returning that literal, ignoring the environment *)
let num = (pfloat |>> (fun z _ -> z))

(* helper function converting a numeric operator to a function of the environment *)
(* fop : (double -> double -> double) -> (env -> double) -> (env -> double) -> env -> double *)
let fop op fa fb env = fa env |> op <| fb env

(* Parse single operators - return function taking two operands and giving the result *)
let (addop : Parser<_,unit>) =
    sym "+" >>% fop (+)
    <|> ( sym "-" >>% fop (-) )
let (mulop : Parser<_,unit>) =
    sym "*" >>% fop (*)                  // Keep syntax highlighter happy! *)
    <|> ( sym "/" >>% fop (/) )

let (atom: Parser<float->float,unit>), atomImpl = createParserForwardedToRef() // break circular reference

(*  Parse math function name - return the function itself *)
let (mathOp: Parser<_,unit>) =
       sym "sin" >>% sin
       <|> ( sym "cos" >>% cos )
       <|> ( sym "tan" >>% tan )

(* f(x) or f(2+3) *)
(* compose result of parsing the expression with the given function *)
let mathExpr = mathOp .>>. atom |>> fun (f,g) -> g >> f

(* term, expr - chain of operators of a given precedence *)
let term = chainl1 atom mulop
let expr = chainl1 term addop
atomImpl := num <|> var  <|> mathExpr <|> between (sym "(") (sym ")") expr

Putting it all together: Get Microsoft Silverlight

(Try writing arithmetic expressions in x using *,/,+,-, sin(), cos() - note spaces are not permitted with this simple parser.)

The source code to this is available here, this requires the free version of Visiblox Charts which can be downloaded here.