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('You clicked on " + d + "');")%>
This renders a 2D bar chart, like so:
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 /// <graph> element. When called, the '<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 /// <graph> element. When called, the '<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?
I admire what you have done here.
Regards
Promi
I recently came across your blog and have been reading along.
Regards
Martin
i do not understand why you could not go with object initializers instead of fluent:
<%= Html.FusionCharts(new Chart()
{
Column2D = new ColumnInfo(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('You clicked on " + d + "');"))%>
much simpler, faster, cleaner and already well understood. fluent apis are a pest that has become "cool".
chaining is a useful concept but it is a workaround for scenarios where object initialization is not possible. fluent nhibernate is also infected by this stupid trend.
I did consider going with an object-initializer approach similar to what you’ve described, but in the end I decided that I actually preferred the look-and-feel of the fluent API. Maybe that’s just me becoming infected by the trend…
Substantially, the post is in reality the greatest on this deserving topic. I concur with your conclusions and will eagerly look forward to your future updates. Saying thanks will not just be adequate, for the exceptional clarity in your writing. I will immediately grab your rss feed to stay abreast of any updates. Pleasant work and much success in your business dealings!
Nice ideas on this webiste. It’s rare nowadays to get websites with information you are trying to find. I am pleased I came upon this webpage. I can definitely bookmark it or perhaps subscribe to your rss feeds simply to be updated on your new posts. Keep up the nice work and I am sure another people looking for valued information can actually stop by and use your website for resources.
I really loved this wonderful article. Please continue this awesome work. Greets!!!!!
Intimately, the post is in reality the freshest on this precious topic. I concur with your conclusions and will thirstily look forward to your coming updates. Just saying thanks will not just be adequate, for the extraordinary lucidity in your writing. I will right away grab your rss feed to stay informed of any updates. Fabulous work and much success in your business enterprise!
This is by far one of the best wrote articles on this content. I was researching on the exact aforesaid subject and your position completely took me off with the way you view this subject area. I compliment your insight but do leave me to come back to comment further as I’m presently extending my research on this subject further. I will be back to join in this discussion as I’ve bookmarked and tag this very page.
We\’ve probably not had the opportunity to get links for my website but I\’ve heard great results this website here in this link is recommended and supposed to be a somewhat exeptional back-links service to <a href="http://www.megafoo.com/web/googletopsearch.com\"> buy backlinks</a>.
I don’t normally comment on blogs, however I have to say that I rather enjoyed yours as it was ensightful. I´ve bookmarked your blog and hope to explorer it further when I have a little more time. Keep up the good work. Well back to my dreaming of Panama (see for yourself at this Harvard website http://blogs.law.harvard.edu/panama/panama) or back to the books – I wonder which one is going to win out. 🙂
Good day everybody, This website is enjoyable and so is the way the subject matter was written about. I like some of the comments also even though I would rather we don’t err from the main point in order add value to the topic. It will be also encouraging to the writer if we all could share it (for those who use bookmarking services such as a digg, twitter,..). Thanks again.
The new Zune browser is surprisingly good, but not as good as the iPod’s. It works well, but isn’t as fast as Safari, and has a clunkier interface. If you occasionally plan on using the web browser that’s not an issue, but if you’re planning to browse the web alot from your PMP then the iPod’s larger screen and better browser may be important.
Gibraltar’s Kaiane Aldorino claimed the title of Miss World 2009 on Saturday, defeating 111 other hopefuls at a glittering ceremony in South Africa. Mexico’s Perla Beltran came second, with South African entry Tatum Keshwar taking third place. – Missy
While this issue can be very tough for most people, my opinion is that there has to be a middle or common ground that we all can find. I do appreciate that you’ve added relevant and rational commentary here though. Very much thanks to you!
While this issue can be very vexed for most people, my thought is that there has to be a middle or common ground that we all can find. I do value that you’ve added pertinent and sound commentary here though. Thank you!
Nice one, there is actually some good facts on this post some of my subscribers just might find this relevant, I must send a link, thank you.
Cool, there are actually some great points on here some of my associates might find this worthwhile, will send a link, many thanks.
many thanks for this valuable info I must post a link on my blog so my readers can benefit from it too.
Nice one, there are actually some great ideas on this post some of my associates will find this relevant, I must send them a link, many thanks.