Archive for the ‘ Business Intelligence ’ Category

Enabling MSChart ImageMap and AJAX functionality

    In a previous post I talked about utilizing MSChart in an SSRS report. In this post we will talk about how to use the image map capabilities of the MSChart control. You will need to create two identical charts, one set with a render type of imagemap, the other set to binarystreaming. The easiest way to do this without having duplicate code is to create a user control containing your chart.

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="graph.ascx.cs" Inherits="graph" %>
<%@ Register Assembly="System.Web.DataVisualization, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Namespace="System.Web.UI.DataVisualization.Charting" TagPrefix="asp" %> 
<asp:chart id="mainChart" runat="server" height="296px" width="412px"
    imagetype="Png" palette="BrightPastel" backcolor="LightBlue" rendertype="BinaryStreaming"
    borderdashstyle="Solid" backgradientstyle="TopBottom" borderwidth="2" bordercolor="181, 64, 1">
    <titles>
        <asp:Title ShadowColor="32, 0, 0, 0" Font="Trebuchet MS, 14.25pt, style=Bold" ShadowOffset="3" Text="Binary Streaming" ForeColor="26, 59, 105"></asp:Title>
    </titles>
    <legends>
        <asp:Legend Enabled="True" IsTextAutoFit="True" Name="Default" BackColor="Transparent" Font="Trebuchet MS, 8.25pt, style=Bold" Alignment="Center" Docking="Bottom"></asp:Legend>
    </legends>
    <borderskin skinstyle="Emboss" />
    <series>
        <asp:Series XValueType="String" Name="Targets" ChartType="SplineArea" BorderColor="180, 26, 59, 105" Color="220, 0, 0, 0" YValueType="Double"/>
        <asp:Series XValueType="String" Name="Actuals" ChartType="SplineArea" BorderColor="180, 26, 59, 105" Color="127, 255, 255, 0" YValueType="Double"/>
        <asp:Series XValueType="String" Name="ActualPoints" ChartType="SplineArea" BorderColor="180, 200, 59, 105" Color="127, 65, 140, 240" YValueType="Double"/>
    </series>
    <chartareas>
        <asp:ChartArea Name="MainChartArea" BorderColor="64, 64, 64, 64" BorderDashStyle="Solid" BackSecondaryColor="White" BackColor="OldLace" ShadowColor="Transparent">
            <axisy linecolor="64, 64, 64, 64">
                <labelstyle font="Trebuchet MS, 8.25pt, style=Bold"/>
                <majorgrid linecolor="64, 64, 64, 64"/>
            </axisy>
            
            <axisx IsMarginVisible="False" linecolor="64, 64, 64, 64">
                <labelstyle font="Trebuchet MS, 8.25pt, style=Bold"/>
                <majorgrid linecolor="64, 64, 64, 64"/>
            </axisx>
        </asp:ChartArea>
    </chartareas>
</asp:chart>

In the code behind of your graph user control you will want to publicly expose the chart control.

 public System.Web.UI.DataVisualization.Charting.Chart MainChart
    {
        get
        {
            return mainChart;
        }
    }

The Page Load event should go about parsing the query string for any data needed to generate the chart itself.

protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            int kpiID = 0;
            int organizationID = 0;
            int programID = 0;
            int fiscalYearID = 0;
            int height = 0;
            int width = 0;

            int.TryParse(Request.QueryString["kpiID"], out kpiID);
            int.TryParse(Request.QueryString["organizationID"], out organizationID);
            int.TryParse(Request.QueryString["programID"], out programID);
            int.TryParse(Request.QueryString["fiscalYearID"], out fiscalYearID);
            int.TryParse(Request.QueryString["height"], out height);
            int.TryParse(Request.QueryString["width"], out width);

            if (height == 0)
                height = 296;

            if (width == 0)
                width = 412;

            mainChart.Height = Unit.Pixel(height);
            mainChart.Width = Unit.Pixel(width);


            string kpiName = Request.QueryString["kpiName"];

            if (kpiID != 0)
                BuildGraph(kpiID, fiscalYearID, organizationID, programID, kpiName);
        }
    }

    The next step is to create two web pages: graph.aspx, and graphImageMap.aspx.  Each page will contain nothing but your newly created graph user control, however, their code behinds will differ slightly.

GraphImage.aspx content:

Page Content for graphImage.aspx:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="graphImage.aspx.cs" Inherits="graphImage" %>
<%@ Register src="ascx/graph.ascx" tagname="graph" tagprefix="uc1" %>
<uc1:graph ID="graph1" runat="server" />

Code Behind for graphImage.aspx:

public partial class graphImage : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            graph1.MainChart.RenderType = System.Web.UI.DataVisualization.Charting.RenderType.BinaryStreaming;
        }
    }
}

GraphImageMap.aspx content:

Page Content for graphImageMap.aspx:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="graphImageMap.aspx.cs" Inherits="graphImageMap" %>
<%@ Register src="ascx/graph.ascx" tagname="graph" tagprefix="uc1" %>
<uc1:graph ID="graph1" runat="server" />

Code Behind for graphImageMap.aspx:

public partial class graphImageMap : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            graph1.MainChart.RenderType = System.Web.UI.DataVisualization.Charting.RenderType.ImageMap;
            graph1.MainChart.ImageLocation = "graphImage.aspx?" + Request.QueryString.ToString();
        }
    }
}

 

In the code behind of the graphImage web page, set the exposed chart control property of the graph user control to binary streaming. In the code behind of the graphImageMap web page, set the exposed chart control property of the graph user control to imagemap. Also, set the ImageLocation property to reference graphImage webpage and pass along the query string. This will ensure that the imagemap page is creating the exact same chart as the image page chart. To try out this new MS Chart functionality, we used JQUERY to setup a real world example. Let’s say you want to pop up an interactive chart when a user hovers over a link on a webpage.

<html>
<head>
 <script type="text/javascript" src="js/jquery-1.4.4.min.js"></script>
    <script type="text/javascript" src="js/jquery-ui-1.8.6.custom.min.js"></script>
    
     <script type="text/javascript">
         function ShowDialog(imgURL) {
             var uniqueTime = new Date().getTime();
             $("#graphImage").attr("src", imgURL + '&time=' + uniqueTime);
             $("#dialog").dialog("open");
         }
         // we will add our javascript code here
         $(document).ready(function() {
             $("#dialog").dialog({
                 autoOpen: false,
                 height: 350,
                 width: 440,
                 modal: false,
                 closeOnEscape: true,
                 show: 'fade',
                 hide: 'fade'
             });


         });

     function ShowDialog(imgURL) {
             $("#graphImage").attr("src", imgURL);
             $("#dialog").dialog("open");
         }
         // we will add our javascript code here
         $(document).ready(function() {

             $(".test").mouseenter(function() {
                 $.ajax({
                     url: "graphImageMap.aspx?kpiID=2004&fiscalYearID=5&organizationID=4&programID=219&kpiname=AmazingMetric",
                     cache: false,
                     async: false,
                     success: function(html) {
                         $("#dialogInner").replaceWith(html);
                     }
                 });
                 $("#dialog").dialog("open");
             });
     </script>  
</head>
<body>
    <a id='link' class="test">hello world</a>
        <div id="dialog" title="My Graph">
            <div id="dialogInner"></div>
        </div>
</body>
</html>

I hope you found this post helpful in creating an interactive MS Charting solution.  Now get coding!

Utilizing MSChart in SSRS

    MSChart has been around for a while now, but we found a new use for it regarding some SSRS (SQL Server Reporting Services) reports we were creating the other day.

    We had a need to generate some charts on the fly based on a particular action taken by a user, like clicking a metric. The MS Chart controls have some really nice capabilities for a free charting package. They support caching, multiple data binding scenarios, and AJAX driven events, just to name a few. We utilized the charts ability to generate a binary stream of data to create our own custom image handler.  In order to try this out yourself, simply create a web page with nothing in it but a chart control like the below example.

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="graph.ascx.cs" Inherits="graph" %>
<%@ Register Assembly="System.Web.DataVisualization, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Namespace="System.Web.UI.DataVisualization.Charting" TagPrefix="asp" %> 
<asp:chart id="mainChart" runat="server" height="296px" width="412px"
    imagetype="Png" palette="BrightPastel" backcolor="LightBlue" rendertype="BinaryStreaming"
    borderdashstyle="Solid" backgradientstyle="TopBottom" borderwidth="2" bordercolor="181, 64, 1">
    <titles>
        <asp:Title ShadowColor="32, 0, 0, 0" Font="Trebuchet MS, 14.25pt, style=Bold" ShadowOffset="3" Text="Binary Streaming" ForeColor="26, 59, 105"></asp:Title>
    </titles>
    <legends>
        <asp:Legend Enabled="True" IsTextAutoFit="True" Name="Default" BackColor="Transparent" Font="Trebuchet MS, 8.25pt, style=Bold" Alignment="Center" Docking="Bottom"></asp:Legend>
    </legends>
    <borderskin skinstyle="Emboss" />
    <series>
        <asp:Series XValueType="String" Name="Targets" ChartType="SplineArea" BorderColor="180, 26, 59, 105" Color="220, 0, 0, 0" YValueType="Double"/>
        <asp:Series XValueType="String" Name="Actuals" ChartType="SplineArea" BorderColor="180, 26, 59, 105" Color="127, 255, 255, 0" YValueType="Double"/>
        <asp:Series XValueType="String" Name="ActualPoints" ChartType="SplineArea" BorderColor="180, 200, 59, 105" Color="127, 65, 140, 240" YValueType="Double"/>
    </series>
    <chartareas>
        <asp:ChartArea Name="MainChartArea" BorderColor="64, 64, 64, 64" BorderDashStyle="Solid" BackSecondaryColor="White" BackColor="OldLace" ShadowColor="Transparent">
            <axisy linecolor="64, 64, 64, 64">
                <labelstyle font="Trebuchet MS, 8.25pt, style=Bold"/>
                <majorgrid linecolor="64, 64, 64, 64"/>
            </axisy>
            
            <axisx IsMarginVisible="False" linecolor="64, 64, 64, 64">
                <labelstyle font="Trebuchet MS, 8.25pt, style=Bold"/>
                <majorgrid linecolor="64, 64, 64, 64"/>
            </axisx>
        </asp:ChartArea>
    </chartareas>
</asp:chart>

    This allows us to create an external image in our SSRS report with a source expression something like = "http://mysite/graph.aspx?metricID=&quot; & Fields!MetricID.Value & "&fiscalYearID=" & Fields!FiscalYearID.Value from a bound dataset within the report. One gotcha we encountered was the need to setup an unattended execution account on the Reporting Server so that external images could be served up. Learn how to configure the unattended execution account here: Configuring the unattended execution account.  While this is a very flexible way to generate custom images on the fly for reports, we decided we wanted a little more interactivity.

    MSChart supports image maps as well as AJAX callbacks allowing the client to interact with the chart images. We utilized this ability to create custom tooltips for commentary at certain datapoints.  This can be accomplished by iterating through the individual datapoints of your series and setting the MapAreaAttributes property.

mainChart.Series["ActualPoints"].Points[count].MapAreaAttributes = "onmouseover=\"DisplayTooltip('" + toolTip + "');\" onmouseout=\"DisplayTooltip('');\"";

    If you intend to use the image map or AJAX callback support, you will be unable to use the image handler approach since the chart will need to generate an image map and possibly an update panel to perform AJAX events. We will address using the image map and AJAX callback functionality in a future post.

Silverlight Pivot Viewer – Automatic Pivot Collection Generation via Customized Pauthor tool

    The Microsoft Silverlight Pivot Viewer is a new innovative way to look at a massive amount of data in a fast visually comparative way. PivotViewer utilizes Microsoft’s Deep Zoom technology which enables it to take advantage of high resolution imagery without taking a hit on performance.

    Lately, we have been utilizing PivotViewer for Business Intelligence applications. If you are using PivotViewer to simply navigate and categorize images, the images themselves aren’t very important. However, if you are intending to utilize PivotViewer for a BI application, you will want to communicate the business information visually, that is the point after all, isn’t it? There are several ways to generate PivotViewer collections. The pivot collection creator for excel (here) provides a fast, simple way to generate PivotViewer collections. The main drawback of the excel tool is that the images for each item must already exist, plus this is still a very manual process. This is where the Pauthor tool comes into play.

    The Pauthor tool is an open source command line tool for generating pivot collections from various formats, such as excel, csv, and cxml formats. Pauthor allows for the generation of pivot images from an html template, as well as the creation of new data sources and target sources that implement an OLE DB driver. We created a SQL Server datasource based on the OleDBCollectionSource.  This allowed us to use tables, or views, or even stored procedures to generate the pivot collection facet categories, items, and collection text.

public class SqlCollectionSource : OleDbCollectionSource
    {
        /// <summary>
        /// Creates a new Excel collection source and sets its <see cref="BasePath"/>.
        /// </summary>
        /// <param name="basePath">the path to the Excel file containing the collection's data</param>
        public SqlCollectionSource(String serverName, String dbName, String collectionTableName, String facetTableName, String itemTableName)
            : base(String.Format(ConnectionStringTemplate, serverName, dbName), ".")
        {
            m_serverName = serverName.ToLowerInvariant();
            m_dbName = dbName.ToLowerInvariant();
            m_collectionTableName = collectionTableName.ToLowerInvariant();
            m_facetTableName = facetTableName.ToLowerInvariant();
            m_itemTableName = itemTableName.ToLowerInvariant();
        }

        protected override void LoadHeaderData()
        {
            this.ConnectionString = String.Format(ConnectionStringTemplate, m_serverName, m_dbName);
            this.UpdateDataQueries();

            base.LoadHeaderData();
        }

        private void UpdateDataQueries()
        {
            //String connectionString = String.Format(ConnectionStringTemplate, this.BasePath);
            using (OleDbConnection connection = new OleDbConnection(this.ConnectionString))
            {
                connection.Open();
                DataTable schema = connection.GetOleDbSchemaTable(
                    OleDbSchemaGuid.Tables, new Object[] { null, null, null, "TABLE" });
                
                String firstTableName = null;
                foreach (DataRow row in schema.Rows)
                {
                    String table = row["Table_Name"].ToString();
                    String lowerTable = table.ToLowerInvariant();
                    if (lowerTable == m_collectionTableName)
                    {
                        this.CollectionDataQuery = String.Format(CommandTemplate, table);
                    }
                    if (lowerTable == m_facetTableName)
                    {
                        this.FacetCategoriesDataQuery = String.Format(SortedCommandTemplate, table);
                    }
                    if (lowerTable == m_itemTableName)
                    {
                        this.ItemsDataQuery = String.Format(SprocTemplate, table);
                    }
                    if (firstTableName == null) firstTableName = table;
                }

                schema = connection.GetOleDbSchemaTable(
                    OleDbSchemaGuid.Procedures, new Object[] { null, null, m_itemTableName, null });

                
                foreach (DataRow row in schema.Rows)
                {
                    String table = row["Procedure_Name"].ToString();
                    table = table.Remove(table.IndexOf(';'));
                    String lowerTable = table.ToLowerInvariant();
                    
                    if (lowerTable == m_itemTableName)
                    {
                        this.ItemsDataQuery = String.Format(SprocTemplate, table);
                    }
                
                }

                if (this.ItemsDataQuery == null)
                {
                    this.ItemsDataQuery = String.Format(CommandTemplate, firstTableName);
                }
            }
        }

        private const String ConnectionStringTemplate = "Provider=SQLNCLI10; Server={0};" +
            "Database={1}; Trusted_Connection=yes;";

        private const String CommandTemplate = "SELECT * FROM [{0}]";
        private const String SortedCommandTemplate = "SELECT * FROM [{0}] order by sort";
        private const String SprocTemplate = "exec [{0}]";
        private String m_serverName = string.Empty;
        private String m_dbName = string.Empty;
        private String m_collectionTableName = string.Empty;
        private String m_facetTableName = string.Empty;
        private string m_itemTableName = string.Empty;
    }

Customizing the HTML template functionality

    The base HTML template functionality allows you to specify an html file incorporating meta tags like {name} {href} and any other custom pivot facet categories your items use within the html itself. Pauthor then iterates through the collection item datasource and creates a new html file for each one by replacing all of these tags with the item data source values. A web browser control is then run within the code to load up the newly generated html file and save an image out to the file system. Deep zoom images are then created for the pivot collection based off of this saved custom image as well as the xml of the pivot collection itself. We also customized the html template functionality allowing for a template query string to be used.

Example: /html-url http://www.example.com/PivotCard?itemPrimaryKey={itemPrimaryKey}

    This immediately increases the flexibility of the images we create, as we can now generate any html we want using standard web technology based on the values we receive off of the query string as opposed to scattering meta tags in an html file. Managing a web page which generates these Pivot Images is much easier to maintain than trying to generate images manually using a graphics library. You can imagine the time involved in changing an image template, moving lines around, drawing text with wrapping capabilities, etc, each time your Pivot Card format changes.

Pivot Collection Generation

    Pivot collection generation is now a breeze! There are no longer any manual steps with having our data in a database and images automatically generated and saved on the web server which serves up the pivot collection.  Collection generation can now be kicked off with a nightly scheduled task or even more frequently if needed. We are currently working on creating a means of generating pivot cards on a one off basis so that the collection can be maintained in near real time without incurring the cost of generating the entire collection each time an item changes. Stay tuned!