Hey, it only took me nearly a month to write part 2 of this series!  Yeah, I’ve been neglecting this blog a lot lately.  There just aren’t enough hours in the day to write. 

In part 1, I discussed charting with ASP.NET MVC and why I decided to use a Flash solution (FusionCharts Free) instead of an ASP.NET control or a JavaScript solution.  Nearly a month later, I’m still very glad I made the switch. 

As I said in part 1, FusionCharts Free includes some methods for working with FusionCharts from ASP.NET, and those work fine in both WebForms and MVC applications.  They aren’t very MVC-like though, and they take quite a few parameters that I don’t want to deal with most of the time.  They also don’t help with building the XML that configures a FusionChart.  The markup the methods generate also suffers from the annoying Flash z-order glitch.  To overcome these limitations, I decided to create some HtmlHelper extensions.  I wanted something that was flexible yet simple.  While I think that fluent APIs have been abused by the .NET community, I do think there are times where they make sense.  When using an HtmlHelper extension, I want to be able to configure the extension without resorting to a code block, which is exactly why I chose to go the fluent route for the FusionCharts helpers.  Before digging into the implementation, let’s take a look at the API in action again.  This is a simple example using an array of numbers, but the API actually supports any object you want to work with:

<%=Html.FusionCharts().Column2D(new[] {1, 2, 3, 4, 5}, 300, 300, d => d)
        .Caption("Numbers")
        .SubCaption("(subcaption)")
        .Label(d => "Label " + d)
        .Hover(d => "Hover " + d)
        .Action(d => "javascript:alert(&apos;You clicked on " + d + "&apos;);")%>

This renders a 2D bar chart, like so:

barChart

All of the methods should be fairly self-explanatory, except perhaps the Action method.  Action allows you to create hyperlinks for the data items in your chart.  In this case, I’ve created a JavaScript link that will display an alert when a bar in the chart is clicked. 

As I said, this API is generic and supports any type.  This allows you to pass in complicated objects that expose values, labels, etc. that can be bound using the various methods exposed on the fluent API. 

Time for some code.  First is the HtmlHelper extension and the entry point to the FusionChart helpers:

/// <summary>
/// Container for the actual extension method.
/// </summary>
public static class FusionChartsHtmlHelper
{
    /// <summary>
    /// Gets a helper for building a fusion chart.
    /// </summary>
    /// <param name="helper"></param>
    /// <returns></returns>
    public static FusionChartsHelper FusionCharts(this HtmlHelper helper)
    {
        return new FusionChartsHelper(helper);
    }
}

/// <summary>
/// An HTML helper for FusionCharts. 
/// </summary>
public class FusionChartsHelper
{
    /// <summary>
    /// The HTML helper.
    /// </summary>
    private readonly HtmlHelper mHtmlHelper;

    /// <summary>
    /// The resolved path to the Fusion Charts SWF files.
    /// </summary>
    private readonly string mChartsFolderBase;

    /// <summary>
    /// Initializes the helper. 
    /// </summary>
    /// <param name="helper"></param>
    public FusionChartsHelper(HtmlHelper helper)
    {
        mHtmlHelper = helper;
        UrlHelper urlHelper = new UrlHelper(helper.ViewContext.RequestContext);

        mChartsFolderBase = urlHelper.Content("~/Charts/");
    }

    /// <summary>
    /// Gets a fusion chart builder that will create a 2D bar chart.
    /// </summary>
    /// <typeparam name="T">The type of the data items.</typeparam>
    /// <param name="data">The items to bind to the chart.</param>
    /// <param name="width">Width in pixels.</param>
    /// <param name="height">Height in pixels.</param>
    /// <param name="getValue">Delegate that extracts the numerical value from a data item.</param>
    /// <returns>A 2D chart builder.</returns>
    public FusionChartColumn2DBuilder<T> Column2D<T>(
        IEnumerable<T> data, 
        int width, 
        int height,  
        Func<T, double> getValue)
    {
        return new FusionChartColumn2DBuilder<T>(mHtmlHelper, mChartsFolderBase, data, getValue, width, height);
    }

    /// <summary>
    /// Creates a builder for 2D pie chart.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="data"></param>
    /// <param name="width"></param>
    /// <param name="height"></param>
    /// <param name="getValue"></param>
    /// <returns></returns>
    public FusionChartPie2DChartBuilder<T> Pie2D<T>(
        IEnumerable<T> data, 
        int width, 
        int height,  
        Func<T, double> getValue)
    {
        return new FusionChartPie2DChartBuilder<T>(mHtmlHelper, mChartsFolderBase, data, getValue, width, height);
    }
}

So far I’ve only implemented two chart builders (one for bar charts, and one for pie charts), but FusionCharts supports a slew of charts that could easily be integrated into this API.

FusionChartsHelper doesn’t do much aside from instantiate the actual chart builders.  These are the fluent APIs for building and configuring a chart.  Let’s look at each of them:

/// <summary>
/// A builder for a 2D column chart.
/// </summary>
/// <typeparam name="T"></typeparam>
public class FusionChartColumn2DBuilder<T> : FusionChartBuilder<T>
{
    /// <summary>
    /// The filename of the chart.
    /// </summary>
    private const string CHART_NAME = "FCF_Column2D.swf";

    /// <summary>
    /// The label for the X axis.
    /// </summary>
    private string mXAxisLabel;

    /// <summary>
    /// The label for the Y axis.
    /// </summary>
    private string mYAxisLabel;

    /// <summary>
    /// Initializes the builder.
    /// </summary>
    /// <param name="helper"></param>
    /// <param name="baseUrl">The URL to the folder that contains the SWF files for Fusion Charts.</param>
    /// <param name="data"></param>
    /// <param name="valueExtractor"></param>
    /// <param name="width"></param>
    /// <param name="height"></param>
    public FusionChartColumn2DBuilder(HtmlHelper helper, string baseUrl, IEnumerable<T> data, Func<T, double> valueExtractor, int width, int height) :
        base(helper, baseUrl + CHART_NAME, data, valueExtractor, width, height)
    {
    }

    /// <summary>
    /// Writes the X and Y axis labels.
    /// </summary>
    /// <param name="xml"></param>
    internal override void WriteGraphProperties(StringBuilder xml)
    {
        if (mXAxisLabel != null) xml.AppendFormat(" xAxisName='{0}'", mXAxisLabel);

        if (mYAxisLabel != null) xml.AppendFormat(" yAxisName='{0}'", mYAxisLabel);
    }

    /// <summary>
    /// Sets the label for the X Axis.
    /// </summary>
    /// <param name="xAxisLabel"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> XAxisLabel(string xAxisLabel)
    {
        mXAxisLabel = xAxisLabel;

        return this;
    }

    /// <summary>
    /// Sets the label for the Y Axis.
    /// </summary>
    /// <param name="yAxisLabel"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> YAxisLabel(string yAxisLabel)
    {
        mYAxisLabel = yAxisLabel;

        return this;
    }
}

/// <summary>
/// A chart builder for 2D pie charts.
/// </summary>
/// <typeparam name="T"></typeparam>
public class FusionChartPie2DChartBuilder<T> : FusionChartBuilder<T>
{
    /// <summary>
    /// The name of the Pie Chart SWF file.
    /// </summary>
    private const string CHART_NAME = "FCF_Pie2D.swf";

    /// <summary>
    /// Flag that controls whether or not labels are shown by pie slices.
    /// </summary>
    private bool mShowLables = true;

    /// <summary>
    /// Initializes the builder.
    /// </summary>
    /// <param name="helper"></param>
    /// <param name="chartUrl"></param>
    /// <param name="data"></param>
    /// <param name="valueExtractor"></param>
    /// <param name="width"></param>
    /// <param name="height"></param>
    public FusionChartPie2DChartBuilder(HtmlHelper helper, string chartUrl, IEnumerable<T> data, Func<T, double> valueExtractor, int width, int height) 
        : base(helper, chartUrl + CHART_NAME, data, valueExtractor, width, height)
    {
    }

    /// <summary>
    /// Writes chart-specific XML settings. 
    /// </summary>
    /// <param name="xml"></param>
    /// <remarks>
    /// Derived classes should override this method to add any chart-specific markup to the
    /// &lt;graph&gt; element.  When called, the '&lt;graph ' markup will have been rendered already.  
    /// </remarks>
    internal override void WriteGraphProperties(StringBuilder xml)
    {
        if (mShowLables) xml.Append(" shownames='1'");
    }

    /// <summary>
    /// Hides the labels from the pie chart.
    /// </summary>
    /// <returns></returns>
    public FusionChartPie2DChartBuilder<T> HideLabels()
    {
        mShowLables = false;

        return this;
    }
}

Each class is fairly short and exposes only the functionality that is specific to its chart type.  All the common functionality and the core of the chart rendering is handled by the base class, FusionChartBuilder<T>: 

/// <summary>
/// Builds a chart.
/// </summary>
public abstract class FusionChartBuilder<T>
{
    ...
    
    /// <summary>
    /// Creates the builder.
    /// </summary>
    /// <param name="data">The data items to build a chart from.</param>
    /// <param name="valueExtractor">Used to get the value from data items.</param>
    /// <param name="helper"></param>
    /// <param name="chartUrl">The URL to the chart.</param>
    /// <param name="width">Chart width.</param>
    /// <param name="height">Chart height.</param>
    public FusionChartBuilder(HtmlHelper helper, string chartUrl, IEnumerable<T> data, Func<T,double> valueExtractor, int width, int height)
    {
        ...
    }

    /// <summary>
    /// Gets the next available color.
    /// </summary>
    /// <returns></returns>
    private string GetNextColor()
    {
        ...
    }

    /// <summary>
    /// Writes chart-specific XML settings. 
    /// </summary>
    /// <param name="xml"></param>
    /// <remarks>
    /// Derived classes should override this method to add any chart-specific markup to the
    /// &lt;graph&gt; element.  When called, the '&lt;graph ' markup will have been rendered already.  
    /// </remarks>
    internal abstract void WriteGraphProperties(StringBuilder xml);

    /// <summary>
    /// Adds an action link to each item. 
    /// </summary>
    /// <param name="actionLink"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> Action(Func<T, string> actionLink)
    {
        ...
    }

    /// <summary>
    /// Sets the ID of the generated chart.
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> Id(string id)
    {
        ...
    }

    /// <summary>
    /// Specify a callback that extracts a friendly label for each item.
    /// </summary>
    /// <param name="getLabel"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> Label(Func<T, string> getLabel)
    {
        ...
    }

    /// <summary>
    /// Enables debug mode.
    /// </summary>
    /// <returns></returns>
    public FusionChartBuilder<T> EnableDebugMode()
    {
        ...
    }

    /// <summary>
    /// Sets the number of decimal places to show.
    /// </summary>
    /// <param name="precision"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> DecimalPrecision(int precision)
    {
        ...
    }

    /// <summary>
    /// When enabled this will round numbers to millions or thousands and add the
    /// corresponding suffix (k for thousands and m for millions).
    /// </summary>
    /// <param name="enabled"></param>
    /// <returns></returns>
    /// <remarks>
    /// Setting this to true corresponds to setting FormatNumberScale='1' on the
    /// graph XML element in FusionCharts.
    /// </remarks>
    public FusionChartBuilder<T> UseDynamicSuffixes(bool enabled)
    {
        ...
    }

    /// <summary>
    /// Sets the value to prepend to all numeric values.
    /// </summary>
    /// <param name="prefix"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> NumberPrefix(string prefix)
    {
        ...
    }

    /// <summary>
    /// Sets the value to append to all numeric values.
    /// </summary>
    /// <param name="suffix"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> NumberSuffix(string suffix)
    {
        ...
    }

    /// <summary>
    /// Builds a string of text to show as a chart item's tooltip.
    /// </summary>
    /// <param name="hoverLabelBuilder"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> Hover(Func<T, string> hoverLabelBuilder)
    {
        ...
    }

    /// <summary>
    /// Renders the chart.
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        //The real work happens here!
        ...
    }

    /// <summary>
    /// Sets the chart caption.
    /// </summary>
    /// <param name="caption"></param>
    public FusionChartBuilder<T> Caption(string caption)
    {
        ...
    }

    /// <summary>
    /// Sets the chart's subcaption.
    /// </summary>
    /// <param name="subCaption"></param>
    public FusionChartBuilder<T> SubCaption(string subCaption)
    {
        ...
    }
}

I’ve omitted the field and property definitions as they’re mundane (and available in the full source below).  The fluent methods are fairly simple and just do the standard “set and return”:

/// <summary>
/// Sets the chart caption.
/// </summary>
/// <param name="caption"></param>
public FusionChartBuilder<T> Caption(string caption)
{
    mCaption = caption;

    return this;
}

The real work occurs in the ToString method.  This method renders the FusionCharts markup and XML configuration data based on the data items and the configuration settings made with the fluent methods.  It also does a quick find-and-replace to correct the z-ordering problem with Flash:

/// <summary>
/// Renders the chart.
/// </summary>
/// <returns></returns>
public override string ToString()
{
    StringBuilder xml = new StringBuilder();

    xml.Append("<graph");

    WriteGraphProperties(xml);

    if (mDecimalPrecision >= 0) xml.AppendFormat(" decimalPrecision='{0}'", mDecimalPrecision);

    if (mUseDynamicSuffixes) xml.AppendFormat(" formatNumberScale='1'");

    if (mPrefix != null) xml.AppendFormat(" numberPrefix='{0}'", mPrefix);

    if (mSuffix != null) xml.AppendFormat(" numberSuffix='{0}'", mSuffix);

    if (mCaption != null) xml.AppendFormat(" caption='{0}'", mCaption);

    if (mSubCaption != null) xml.AppendFormat(" subCaption='{0}'", mSubCaption);

    xml.AppendLine(">");

    foreach (T item in Data)
    {
        xml.AppendFormat("<set value='{0}' color='{1}'", mValueExtractor(item), GetNextColor());

        if (mLabeler != null)
        {
            xml.AppendFormat(" name='{0}'", mHelper.UrlEncode(mLabeler(item)));
        }

        if (mLinkBuilder != null)
        {
            xml.AppendFormat(" link='{0}'", mHelper.UrlEncode(mLinkBuilder(item)));
        }

        if (mHoverLabelBuilder != null)
        {
            xml.AppendFormat(" hoverText='{0}'", mHelper.UrlEncode(mHoverLabelBuilder(item)));
        }

        xml.AppendLine("/>");
    }

    xml.AppendLine("</graph>");

    string markup = InfoSoftGlobal.FusionCharts.RenderChartHTML(mChartUrl, "", xml.ToString(), mChartId, 
                                                                Width.ToString(), Height.ToString(), mDebugEnabled);

    //We have to add another param to make sure the flash object doesn't shine through jQuery UI.
    markup = markup.Replace("<param name=\"quality\" value=\"high\" />",
                            "<param name=\"quality\" value=\"high\" /><param value=\"opaque\" name=\"wmode\" />")
        .Replace("<embed", "<embed wmode=\"opaque\"");

    return markup;
}

Also note that it calls the abstract WriteGraphProperties method, which allows derived classes to inject their own config settings.

Again, this is a pretty bare-bones API right now.  I’ve implemented just the charts and config options that I needed for the project I’m working on, but it could easily be extended with new settings and chart types.  If you’re going to use this in your own project, be sure you put the SWF files for FusionCharts in ‘/Charts’, or edit the hard-coded path in the code (or replace it with a config setting or static property that can be set from Global.asax). 

You can download the code here, graciously donated by InRAD.  Note that I have not tested this code outside of InRAD’s projects, so let me know if I missed a dependency. 

Thoughts?  Suggestions?