How to Include DHTMLX Gantt Chart Into Skuid Pages

  • 5
  • Idea
  • Updated 3 years ago
  • (Edited)


On a project I'm working on I had to embed a gantt chart. After some research I found a few JS gantt libraries, which didn't turn out of much value. Until I found the DHTMLX Gantt Chart library. It's for free and a pro-version can be bought (I just used the free version, it offers all the functionality I needed).

In order to embed the gantt chart you will have to do the following:
1. Download and unzip the DHTMLX Gantt Chart (you can get it here)
2. Upload the libraries and the css you need to use (in my case dhtmlxgantt.css, dhtmlxgantt.js, ext/dhtmlxgantt_marker.js, ext/dhtmlxgantt_tooltip.js) as static resource (if you want you can also  upload any theme from skins/ but then you don't need the standard css).
3. Include the static resources in your skuid pages and make snippet in which you initialise the chart.
4. Make a template field with the following content (very important: do not select a model! If you do so, the chart will be rendered every time you change some values on the gantt chart. And you might want to alter the style):
<script>    
skuid.snippet.getSnippet('ganttRenderer')();
</script>
<div id="gantt_chart" style="width:100%; height:65vh; overflow: hidden;" />
In my case, the gantt chart is on a tab view. That's why there is a script-tag required. If you have the gantt chart on a page which loads on pageload, you can just delete that script and make an inline snippet.
5. Create a snippet where you initialise the chart (I'm not going to post the whole script at once, I will just guide you through it step by step):
5.1: Declare stuff you will use later (like models, objects, arrays and so on...). :
var $ = skuid.$,
    data = [], //data array later used to display on the gantt chart, contains objects
    startDate = new Date(), //date declarations in order to display the format in the european way
    endDate = new Date(),
    duration = 0, //duration of the task displayed on the gantt chart (in days)
    progress = 0, //the progress of each task
    ganttItem = {}, //an empty object (for now) which contains all the properties for the gantt chart to display
    itemsModel = skuid.$M('the_project_tasks'),
    tasksModel = skuid.$M('the_project_substasks'),
    projectModel = skuid.$M('the_project_model'), //the top most item (in the picture "Test Dev Project")
    project = projectModel.getFirstRow(),
    projectStart = new Date(project.Project_Start__c),
    projectEnd = new Date(project.Project_End__c),
    one_day=1000*60*60*24; //one day in ms (used for duration calculation)
projectStart.setHours(0); //"format" the start and end date. The gantt chart shows hours, so if you need to display the hours you can leave that out
projectStart.setMinutes(0);
projectEnd.setHours(24);
projectEnd.setMinutes(0);
5.2:  Declare a function which assembles the data items:
//returns an object with all the properties needed for the gantt chartfunction
assembleDataObject(start, end, text, comp, id, parent, color){
    endDate = new Date(end);  startDate = new Date(start);     progress = comp / 100;
    if(!isNaN(startDate.getTime()) && !isNaN(endDate.getTime())){ //this'll prevent the gantt chart from breaking and displaying errors (about start date being not a date). If you can guarantee that there won't be empty dates you can leave that out
        startDate.setHours(0);
        startDate.setMinutes(0);
        endDate.setHours(24);
        endDate.setMinutes(0);
   return {
       id: id,
       text: text,
       start_date: startDate, //the start date
       duration: calculateDuration(startDate, endDate), //there is no end date, just a number of days (duration) which the chart then automatically calculates the end date
       progress: comp / 100, //the progress is a decimal number 1 = 100%, 0 = 0%
       open: false, //defines wether the task is collapsed on rendering or not
       parent: parent, //the parent task
       color: color //the color in which the task is displayed (hex or some other value)
   };
    }
}
5.3: Declare some other utility functions:
//requires start and end to be a js date
function calculateDuration(start, end){
    return Math.ceil((end.getTime() - start.getTime()) / (one_day));
}
function updateSowModels(){
    skuid.$M('SowItems').updateData();
    skuid.$M('SowTasks').updateData();
}
//returns an object with the appropriate sow model and record
function getSowRecordAndModel(id){
    var retObj = {},
        project = projectModel.getRowById(id),
        sowItem = itemsModel.getRowById(id),
        sowTask = tasksModel.getRowById(id);
    if(project){
        retObj.model = projectModel;
        retObj.record = project;
        retObj.prog = project.Items_Complete_Percent__c;
        retObj.noOfComps = project.No_Project_Items__c;
        retObj.text = 'Name';
        retObj.start = 'Project_Start__c';
        retObj.end = 'Project_End__c';
    } else if(sowItem){
        retObj.model = itemsModel;
        retObj.record = sowItem;
        retObj.prog = sowItem.Components_Complete__c;
        retObj.noOfComps = sowItem.No_Components__c;
        retObj.text = 'Project_Component__c';
        retObj.start = 'Component_Start_Date__c';
        retObj.end = 'Component_End_Date__c';
    } else if(sowTask){
        retObj.model = tasksModel;
        retObj.record = sowTask;
        retObj.comp = 'Complete__c';
        retObj.text = 'Name';
        retObj.start = 'Item_Start_Date__c';
        retObj.end = 'Item_End_Date__c';
    }
    return retObj;
}
5.4: Initialise the data:
//push the project data as overall parent object
data.push({
    id: project.Id,
    text: project.Name,
    start_date: projectStart,
    duration: calculateDuration(projectStart, projectEnd),
    progress: project.Items_Complete_Percent__c / 100,
    color: '#3d99da', //give it a default good looking color
    open: false //is not opened
});
//iterate over all sow-items and generate the data objects for all items
$.each(itemsModel.getRows(), function(index, item){
    ganttItem = assembleDataObject(item.Component_Start_Date__c, item.Component_End_Date__c, item.Project_Component__c, item.Components_Complete__c, item.Id, project.Id, item.Item_Color__c);
    if(ganttItem){
        data.push(ganttItem);
    }
});
//iterate over all sow-tasks and generate the data objects for all tasks
$.each(tasksModel.getRows(), function(index, task){
    ganttItem = assembleDataObject(task.Item_Start_Date__c, task.Item_End_Date__c, task.Name, task.Complete__c, task.Id, task.Project_Detail__c, task.Project_Detail__r.Item_Color__c);
    if(ganttItem){
        data.push(ganttItem);
    }
});
var tasks = {
    data: data //there are some other properties on that object which I don't use (or not yet at least...)
};
5.5: Set chart properties, tooltips, templates, other styles, etc...
//convert the date to a appropriate format
gantt.config.date_grid = '%d.%m.%Y';
//disable resize
gantt.config.drag_resize = false;
//disable moving around the tasks
gantt.config.drag_move = false;
//disallow dragging around tasks, only allow progress to be altered
gantt.attachEvent("onAfterTaskDrag", function(id, mode, e){
    if(mode == 'progress'){
        var sow = getSowRecordAndModel(id),
            ganttTask = gantt.getTask(id),
            comp = Math.round((ganttTask.progress * 100)*100) / 100;
        if(sow.comp == 'Complete__c'){
            sow.model.updateRow(sow.record, {'Complete__c': comp});
            sow.model.save({'callback': function(){
                updateSowModels();
                gantt.message(ganttTask.text + ': progress updated to ' + comp + '%');
            }});
            return true;
        } else {
            ganttTask.progress = sow.prog / 100;
            gantt.updateTask(id);
            gantt.message({
                type: 'error',
                text: "The Progress of SOW-Items or Dev-Projects can't be updated"
            });
            return false;
        }
    }
    return false;
});
//after edit/view details with double click save the changes
gantt.attachEvent("onLightboxSave", function(id, task, is_new){
    var sow = getSowRecordAndModel(id), updates = {}, start = new Date(task.start_date), end = new Date(task.end_date);
    start.setHours(8);
    end.setHours(17);
    updates[sow.text] = task.text;
    updates[sow.start] = start;
    updates[sow.end] = end;
    sow.model.updateRow(sow.record, updates);
    sow.model.save({'callback': function(){
        gantt.message(task.text + ': changes saved');
        updateSowModels();
    }});
    return true;
});
//configure tooltip
gantt.templates.tooltip_text = function(start, end, task){
    var startDate = new Date(start),
        endDate = new Date(end),
        sow = getSowRecordAndModel(task.id),
        noOfComps = '';
    if(sow.noOfComps){
        noOfComps = "<b>Number of Subtasks:</b> " + sow.noOfComps + "<br />";
    }
    endDate.setDate(endDate.getDate() - 1);
    return "<b>" + task.text+"</b><br/>" +
    noOfComps +
    "<b>Duration:</b> " + task.duration + " Days<br />" +
    "<b>Complete:</b> " + Math.round((task.progress * 100)*100) / 100 + "%<br />" +
    "<b>Start-Date:</b> " + skuid.time.formatDate('dd.mm.yy', startDate) + "<br />" +
    "<b>End-Date:</b> " + skuid.time.formatDate('dd.mm.yy', endDate);
};
//mark weekends with a special color
gantt.templates.scale_cell_class = function(date){
    if(date.getDay() === 0 || date.getDay() == 6){
        return "weekend";
    }
};
gantt.templates.task_cell_class = function(item,date){
    if(date.getDay() === 0 || date.getDay() == 6){ 
        return "weekend" ;
    }
};
5.6. (Finally) Initialise the gantt chart and set a marker on the current date:
//init the gantt chart
gantt.init("gantt_chart");
//display the data on the chart
gantt.parse(tasks);
//gantt.scrollTo(gantt.posFromDate(new Date()), 0);
//after 250ms click on the expand icon (this is needed in order to display the data after rendering. There's a bug which prevents the data from being rendered until a DOM-change occurs...)
setTimeout(function(){
    $('.gantt_open').click();
}, 250);
var date_to_str = gantt.date.date_to_str(gantt.config.task_date),
    id = gantt.addMarker({start_date: new Date(), css: "today", text: 'Today'});
setInterval(function(){
    var today = gantt.getMarker(id);
    today.start_date = new Date();
    today.title = date_to_str(today.start_date);
    gantt.updateMarker(id);
}, 60000);
If someone has more questions on how to do it (or wants the complete script) just ask me.

Cheers
Photo of David Giger

David Giger

  • 1,758 Points 1k badge 2x thumb

Posted 3 years ago

  • 5
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
You are the man David!
Photo of David Giger

David Giger

  • 1,758 Points 1k badge 2x thumb
Wanna see some live demo?
Photo of mB Pat Vachon

mB Pat Vachon, Champion

  • 42,714 Points 20k badge 2x thumb
Yes I would. Sometime Thursday or Friday. Ping me on Skype to make plans.
Photo of David Giger

David Giger

  • 1,758 Points 1k badge 2x thumb
Will do
Photo of David Giger

David Giger

  • 1,758 Points 1k badge 2x thumb
Note:
The export functions are a little strange.
You have to include the following library as external resource (you could also download the file and upload it as static resource): https://export.dhtmlx.com/gantt/api.js and then make a snippet to run on button click (for each one you need/want) containing just one of the following function calls: gantt.exportToPDF(), gantt.exportToPNG() or gantt.exportToExcel() and execute the snippets on button click. Unfortunately there is no better solution to this.
The export functions will open a new tab (external webpage) and generate the file you selected.
(Edited)
Photo of Rob Hatch

Rob Hatch, Official Rep

  • 44,006 Points 20k badge 2x thumb
David this is super cool!