Tuesday, November 18, 2008

ColdFusion 8 - Creating a Simple Gantt Chart

I needed to create a basic Gantt Chart from a query. So once again I dipped into the webcharts3d utility, borrowing code from a great entry about charts on Raymond Camden's blog.

I made some small modifications. Using the webcharts utility, I selected the XMLStyle tab and replaced a few dynamic values with {placeholders} . Then saved only the style information to an xml file. That way I could read in the style information directly, without having to extract it from a .wcp file.



Next, I created two functions. One to convert my query into webchart's xml format. The other to encapsulate Raymond Camden's code to generate the chart html. Since I needed some flexibility with the formatting, the functions have a number of optional settings for things like date formats and locale. But even with the minimal settings it produces a nice and simple Gantt Chart.



Unfortunately, the one thing I could not figure out was how to localize the dates in the chart tool tips. The webcharts3d documentation shows how you can use patterns to format tool tip values. Patterns definitely come in handy. Especially date patterns.


<!--- example of using patterns in chart style xml file --->
<![CDATA[
$(prevValue;EEE MMMM d, yyyy)
$(nextValue;EEE MMMM d, yyyy)
]]>



The problem is the date patterns seem to be applied without regard to the current Locale. They are always formatted according to the default Locale of the jvm. For example, I created a chart using es_ES: Spanish (Standard). While the date labels are handled correctly, the tool tips are formatted using English (US), because that is my server's default.



Very odd. I am hoping I overlooked something. I cannot imagine the utility can format the date labels correctly, but *whoops* .. we forgot about tool tips. Drat. That small item mars an otherwise perfect chart. Oh, well I guess I will keep looking.

Updated: November 19 - Corrected copy / paste error
Updated: March 1, 2010 - Modified date/time formatting

Generate Gantt Chart
<!---
    CREATE QUERY WITH SAMPLE DATA
--->
<cfset rangeStart = createDate(2008,10,1)>
<cfset rangeEnd = createDate(2008,10,31)>
<cfset numOfDays = 31>
<cfset qTasks = QueryNew("TaskName,Type,StartDate,EndDate")>
<cfloop from="1" to="6" index="t">
    <cfloop list="Testing,Development" index="type">
        <cfset row = queryAddRow(qTasks, 1)>
        <cfset d1 = randRange(1, 31)>
        <cfset d2 = randRange(1, 31)>
        <cfset taskStart = createDate(2008, 10, min(d1, d2))>
        <cfset taskEnd = createDate(2008, 10, max(d1, d2))>

        <cfset querySetCell(qTasks, "TaskName", "Task "& t, row)>
        <cfset querySetCell(qTasks, "Type", type, row)>
        <cfset querySetCell(qTasks, "StartDate", taskStart, row)>
        <cfset querySetCell(qTasks, "EndDate", taskEnd, row)>
    </cfloop>
</cfloop>

<!---
    GENERATE CHART HTML
--->
<cfset chartProp = {    stylePath = ExpandPath("./simpleGanttStyle.xml"),
                        query = qTasks,
                        nameCol = "TaskName",
                        typeCol = "Type",
                        startCol = "StartDate",
                        endCol = "EndDate",
                        height = 400,
                        width = 800
    }>

<cfset chartHTML = generateGantt( argumentCollection = chartProp )>

<!--- display chart --->
<cfoutput>#chartHTML#</cfoutput>


simpleGanttStyle.xml (Chart Style)
<?xml version="1.0" encoding="UTF-8"?>
<gantt>
     <xAxis isMultiline="true">
         <dateTimeStyle majorUnit="Day" minorUnit="Hour" />
         <groupFormat style="DateTimePattern" pattern="{groupDatePattern}">
                <locale lang="{language}" country="{country}" variant=""/>
           </groupFormat>
             <labelFormat style="DateTimePattern" pattern="{labelDatePattern}">
                <locale lang="{language}" country="{country}" variant=""/>
           </labelFormat>
     </xAxis>

     <dataLabels style="Value"/>
     <popup background="#C8FFFFFF" foreground="#333333" isMultiline="true"/>
     <paint palette="Dawn" paint="Plain" max="100"/>
 <![CDATA[
$(rowLabel)
/ $(colLabel)
$(prevValue;{tipDatePattern})
$(nextValue;{tipDatePattern})
 ]]>
</gantt>


Functions
<cffunction name="generateGantt" returntype="string">
    <cfargument name="stylePath" type="string" required="true" hint="Absolute path the xml chart style">
    <cfargument name="query" type="query" required="true">
    <cfargument name="nameCol" type="string" required="true">
    <cfargument name="typeCol" type="string" required="true">
    <cfargument name="startCol" type="string" required="true">
    <cfargument name="endCol" type="string" required="true">
    <cfargument name="width" type="numeric" required="true">
    <cfargument name="height" type="numeric" required="true">
    
    <cfargument name="format" type="string" required="false" default="png">
    <cfargument name="language" type="string" required="false" default="">
    <cfargument name="country" type="string" required="false" default="">
    <cfargument name="groupDatePattern" type="string" required="false" default="MMM-yyyy">
    <cfargument name="labelDatePattern" type="string" required="false" default="E dd">
    <cfargument name="tipDatePattern" type="string" required="false" default="">

    <cfset var Local = {} >

    <cfscript>
        Local.Locale = createObject("java", "java.util.Locale");
        // Create a Locale using the given language / country
        if ( len( arguments.language ) and len( arguments.country ) ) {
            Local.dateLocale = Local.Locale.init( arguments.language, arguments.country );
        }
        // Use the current Locale
        else {
            Local.dateLocale = getPageContext().getResponse().getLocale();
            if ( not structKeyExists(Local, "dateLocale") ) {
                Local.dateLocale = Local.Locale.getDefault();
            }    
        }
    
        arguments.dateLocale = Local.dateLocale;
        arguments.language = Local.dateLocale.getLanguage();
        arguments.country = Local.dateLocale.getCountry();
        arguments.model = queryToGanttModel( argumentCollection= arguments );
        Local.chartHTML = generateGanttHTML( argumentCollection = arguments );
    </cfscript>

    <cfreturn Local.chartHTML>
</cffunction>


<cffunction name="generateGanttHTML" returntype="string">
    <cfargument name="stylePath" type="string" required="true">
    <cfargument name="model" type="string" required="true">
    <cfargument name="height" type="numeric" required="true">
    <cfargument name="width" type="numeric" required="true">
    <cfargument name="format" type="string" required="false" default="png">
    <cfargument name="language" type="string" required="true" default="en">
    <cfargument name="country" type="string" required="false" default="US">
    <cfargument name="groupDatePattern" type="string" required="false" default="MMM-yyyy">
    <cfargument name="labelDatePattern" type="string" required="false" default="EEE dd">
    <cfargument name="tipDatePattern" type="string" required="false" default="">
    
    <cfset var Local = {}>
    
    <cfscript>
        // Read in the xml styles and set the desired date patterns
        // Note, the date patterns _are_ case sensitive
        Local.style = FileRead( arguments.stylePath );
    
        // SOURCE: Raymond Camden blog entry
        // SOURCE: http://www.coldfusionjedi.com/index.cfm/2008/1/18/Coolest-CFCHART-Trick-Ever
        Local.style = replaceNoCase( Local.style, "{groupDatePattern}", arguments.groupDatePattern );
        Local.style = replaceNoCase( Local.style, "{labelDatePattern}", arguments.labelDatePattern );
        Local.style = replaceNoCase( Local.style, "{language}", arguments.language, "all" );
        Local.style = replaceNoCase( Local.style, "{country}", arguments.country, "all" );
        Local.style = replaceNoCase( Local.Style, ";{tipDatePattern}", ";"& arguments.tipDatePattern, "all" );
    
        Local.MxServerComponent = createObject( "java", "com.gp.api.jsp.MxServerComponent" );
        Local.context = getPageContext().getServletContext();
        Local.chartServer = Local.MxServerComponent.getDefaultInstance( Local.context );
    
        Local.chart = Local.chartServer.newImageSpec();
        Local.chart.width = arguments.width;
        Local.chart.height = arguments.height;
        Local.chart.type = arguments.format;
        Local.chart.style = Local.style;
        Local.chart.model = arguments.model;
    
        // Get host information
        if ( getPageContext().getRequest().isSecure() ) {
            Local.baseURL = "https://"& getPageContext().getRequest().getHeader('Host') &"/";
        }
        else {
            Local.baseURL = "http://"& getPageContext().getRequest().getHeader('Host') &"/";
        }
    </cfscript>
    
    <!--- Create html tag set --->
    <cfsavecontent variable="Local.chartHTML">
        <cfoutput>#Local.chartServer.getImageTag( Local.chart, Local.baseURL & "CFIDE/GraphData.cfm?graphCache=wc50&graphID=")#</cfoutput>
    </cfsavecontent>
    
    <!--- Good old Webcharts loves to add an extra /Images/ to the URL --->
    <cfset Local.chartHTML = replace( Local.chartHTML, Local.baseURL & "Images/", Local.baseURL , "all" )>
    
    <cfreturn Local.chartHTML>
</cffunction>


<cffunction name="queryToGanttModel" returntype="string">
    <cfargument name="query" type="query" required="true">
    <cfargument name="nameCol" type="string" required="true">
    <cfargument name="typeCol" type="string" required="true">
    <cfargument name="startCol" type="string" required="true">
    <cfargument name="endCol" type="string" required="true">
    <cfargument name="dateLocale" type="any" required="true">
    
    <cfset var Local = {}>

    <cfscript>
        // use formatter to convert date objects into strings
        // Note: This constructor may not support all locales    
        // Note: Date patterns _are_ case sensitive
        Local.datePattern = "yyyy-MM-dd HH:mm:ss"; 
        Local.SimpleDateFormat = createObject("java", "java.text.SimpleDateFormat");
        Local.formatter = Local.SimpleDateFormat.init( Local.datePattern, arguments.dateLocale );
        Local.formatter.setLenient( false );
            
        // prepare to generate the xml
        Local.newline = createObject("java", "java.lang.System").getProperty("line.separator");
        Local.sb = createObject("java", "java.lang.StringBuffer").init();
    
        //    generate the xml headers
        //  note, newlines are added for 'readability'
        Local.sb.append('<?xml version="1.0" encoding="UTF-8"?>');
        Local.sb.append( Local.newline );
        Local.xmlLocale = arguments.dateLocale.getLanguage() &"-"& arguments.dateLocale.getCountry();
        Local.sb.append('<xml pattern="'& Local.datePattern &'" locale="'& Local.xmlLocale &'">');
        Local.sb.append( Local.newline );
    
        // generate the xml chart content using the query data
        for ( Local.row = 1; Local.row <= arguments.query.recordCount; Local.row++) {
            // extract the time (ie number of milliseconds) from the date column values
            Local.startDate = arguments.query[arguments.startCol][Local.row].getTime();
            Local.endDate = arguments.query[arguments.endCol][Local.row].getTime();
    
            // construct an xml element for each row in the query
            Local.sb.append('<item name="'& arguments.query[arguments.nameCol][Local.row] &'"');
            Local.sb.append(' type="'& arguments.query[arguments.typeCol][Local.row] &'"');
            Local.sb.append(' from="'& Local.formatter.format( Local.startDate ) &'"');
            Local.sb.append(' to="'& Local.formatter.format( Local.endDate ) &'" />');
            Local.sb.append( Local.newline );
        }
    
        //     append closing tag
        Local.sb.append( '</xml>' );
    </cfscript>

    <cfreturn Local.sb.toString()>
</cffunction>

...Read More

Friday, November 14, 2008

CFSELECT required="true" (Well .. not really)

I was never a big fan of the old html cfforms, and consequently never learned much about them. Though they have probably improved over time. While googling I came across the old question of why the required attribute does not work unless the list supports multiple selections. Having a few minutes to kill I decided to explore it.

The common response was along the lines of "You should not expect it to work. Something is always selected if the list size is one". Technically, that is true. But to me it comes down to the fact that the validation was implemented differently than some people might expect, given how they use select lists. The validation code in /CFIDE/scripts/cfform.js is clearly geared towards multiple selection lists, not single.


// if this form field is a select list
if(_c=="SELECT"){

// verify at least one item was selected
for(i=0;i<_b.length;i++){
if(_b.options[i].selected){
return true;
}
}

return false;
}
...


Now I saw a few examples of tweaking cfform.js to produce the desired results. But they were not exactly what I had in mind. If I were to change the script, I would use "" as the default to represent no-selection. But have an optional attribute called "noSelection". So you could use other values like zero (0), etcetera when needed.


/*
REPLACEMENT JAVASCRIPT
*/
if( _c == "SELECT" ) {
var idx = _b.selectedIndex;
var isValid = false;
var _defaultValue = "";

// use the supplied default value
if ( document.getElementById && _b.getAttribute("noSelection") ) {
_defaultValue = _b.getAttribute("noSelection").toLowerCase();
}

if ( _b.type == "select-one" ) {
// something other than the default value is selected
isValid = ( _b.options[idx].value.toLowerCase() != _defaultValue );
}
else {
// multiple list: at least one item is selected
isValid = ( idx >= 0 );
}
return isValid;
}
...


<!---
Use the default "" to represent no selection
--->
<cfform name="someForm" format="xml" skin="basic">
<cfformgroup type="horizontal">
<cfselect name="Company" label="Company" required="true">
<option value="">-- select ---</option>
<option value="1">Company A</option>
<option value="2">Company B</option>
</cfselect>
<cfinput type="submit" name="submitBtn" value="Submit" >
</cfformgroup>
</cfform>

<!---
Use "0" to represent no selection
--->
<cfform name="someForm" format="xml" skin="basic">
<cfformgroup type="horizontal">
<cfselect name="Company" label="Company" required="true" noSelection="0"
message="Do not pass go. Do not collect $200. Not until you select a Company.">
<option value="0">-- select ---</option>
<option value="1">Company A</option>
<option value="2">Company B</option>
</cfselect>

<cfinput type="submit" name="submitBtn" value="Submit" >
</cfformgroup>
</cfform>

Now I do not really have a need for this. But if I ever do, I now know where to look ;)



...Read More

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep