Jan 12, 2012

Chart Control to Add Charts Without Writing New Code

A couple of years ago, I created a control that took chart data and used the Google Chart API to create a custom chart image inside Salesforce. That worked nicely, but I was happy to see that Salesforce finally added charting elements to visualforce in Winter '12.

Once I was added to the charting pilot, I spent some time exploring the new elements. Generally, it is a good start, but there are several features I would like to see (like customizable tooltips), and there are a few bugs that need to be cleaned up (for example, long labels are chopped when displayed in slice tooltips).

When I finished exploring and started creating some of these charts for a customer, I realized that most of the code behind for gathering and displaying chart data is almost identical. I like modular code, so I saw this as an opportunity to create a wrapper control that can take a few parameters and create the chart for you without having to write new apex/soql.

This code sample creates a pie chart when you give it the following parameters:
  • Field - The field name (standard or custom) that is grouped into pie slices.
  • Object - The object (standard or custom) that includes the above field.
  • Conditions - A SOQL string that contains conditions for the query (if needed).
  • Title - A title to display above the chart.
  • Height - The height of the chart area.
  • Width - The width of the chart area.
This control takes care of the null group (no value set), empty dataset, and allows you to add a good range of conditions. Other than that, it is pretty simple. Once set up though, it is easy to add some more bells and whistles. For example, in a later version, I added the ability to reference lookup fields for labels, and the ability to use the data with different chart types.

Step one:
Create an apex class Chart.cls to hold the code for the visualforce components:

public with sharing class Chart {

    // constructor
    public Chart() { }
    // set up properties to get the values from the calling vf page
    public string fld { get; set; }
    public string obj { get; set; }
    public string conditions { get; set; }
    public string title { get; set; }
    public string width { get; set; }
    public string height { get; set; }

    // class to hold data for the display 
    public class ChartData {

        public String label { get; set; }
        public Integer value { get; set; }

        public ChartData(String label, Integer value) {
            this.label = label;
            this.value = value;

    private List Counts;
    public List getCounts() {
        if (Counts == null) {
            if (fld !=null && obj != null) {
                Counts = new List();
                // construct the query for the results. add escape chars to conditions
                string strQuary = 'SELECT Count(Id), ' + fld + ' FROM ' + obj;
                if (conditions != null && conditions.length() > 0)
                    strQuary += ' WHERE ' + conditions.replaceall('\'', '\\\'');
                strQuary += ' GROUP BY ' + fld;

                AggregateResult[] groupedResults = Database.query(strQuary);
                if (groupedResults == null || groupedResults.IsEmpty())
                    // no results, just create a dummy chart
                    Counts.add(new ChartData('No Data', 1));
                    for (AggregateResult ar : groupedResults)
                        // construct a list of the counts
                        if (string.valueof(ar.get(fld)) == null)
                            // capture the count for no value
                            Counts.add(new ChartData('No Value', integer.valueof(ar.get('expr0'))));
                            Counts.add(new ChartData(string.valueof(ar.get(fld)), integer.valueof(ar.get('expr0'))));
        return Counts;
Step two:
Create a component Chart.component:

<apex:component controller="Chart">
    <apex:attribute name="Field" type="String" required="true" assignTo="{!fld}"
                    description="The field name (standard or custom) that is grouped into pie slices." />
    <apex:attribute name="Object" type="String" required="true" assignTo="{!obj}"
                    description="The object (standard or custom) that includes the above field." />
    <apex:attribute name="LookupField" type="String" required="false" assignTo="{!lookupFld}"
                    description="If the DataField is a lookup, this is the field holds the values that the lookup resolve to." />
    <apex:attribute name="LookupObject" type="String" required="false" assignTo="{!lookupObj}"
                    description="If the Field is a lookup, this is the object that LookupField is in." />
    <apex:attribute name="Conditions" type="String" required="false" assignTo="{!conditions}"
                    description="A SOQL string that contains conditions for the query (if needed)." />
    <apex:attribute name="Title" type="String" required="false" assignTo="{!title}"
                    description="A title to display above the chart." />
    <apex:attribute name="height" type="String" required="false" assignTo="{!height}" default="220"
                    description="The height of the chart area." />
    <apex:attribute name="width" type="String" required="false" assignTo="{!width}" default="300"
                    description="The width of the chart area." />

    <span style="font-size: 14px; font-weight: bold;">{!Title}</span><br/>
    <apex:chart height="{!height}" width="{!width}" data="{!Counts}">
        <apex:pieSeries dataField="value" labelField="label"/>
        <apex:legend position="right"/>

Step three:
Include the component in your visualforce page like that:

<c:Chart Field="Status__c" Object="CustomObj__c" Conditions="IsArchived__c = False" title="My Status" />