10 July 2006

Elastic SIMILE Timeline - Project Tasks

Last week, I came across a blog entry on Ajaxian in regards to a new ajax calendar project TimeLine by SIMILE, I was blown away. I've been searching for a good calendar control for what seemed like forever, let along a free one (BSD Licenses too!) I'm developing a project management software and I'd kill for a cool calendar control so I can add task / milestone/ project to a user-friendly, super cool calendar control.

I did a quick test run and there is one little "issue". SIMILE timeline is based on static xml files/JSON which is very nice. However, you have to anticipate how big your timeline container is to accomdate all the "events", otherwise, some events will get cut off. 

My friend Jackson is a great css designer and I sought his help as how could I adjust the output and make the timeline expand. After some debugging, he pointed out to me that there are three "divs" that I need to adjust on-the-fly for this to work.

  1. The top container where you explicitly defined the container, adjust it's "height"
  2. The div container with classname "timeline-band", adjust its "height"
  3. Every div container after #2, adjust its "top" accordingly.

Having this knowledge in my mind, I implemented the following, a project task demo can be seen here where you can add unlimited tasks dynamically and vertically. 

*Note: There are still some minor issues left as the bottom of the chart gets overlapped a little bit in the first few "add" if the div is around 150px , should be easy to fix. Also, the Monthly View [Month] label slowly gets cut off as you add.

1. TimeLine.js

This function is used to re-distrbute the timeline height eveyrtime when you add an event.

Timeline._Impl.prototype._distributeWidthsUponEvent = function(index)
{

//1. Increase container div length by default 20.5;

var increaseby = 20.5;
var heightArray = this._containerDiv.style.height.split("px");
var height = parseInt(heightArray[0])+increaseby;
this._containerDiv.style.height = height+"px";

for (var i = 0; i < this._bands.length; i++) {


var
band = this._bands[i];
var currentTop = band.getBandTop();
var currentHeight = band.getBandHeight();

if(i==index){

//2. Increase current container height

var newHeight = currentHeight + increaseby;
band.setBandShiftAndWidth(currentTop, newHeight);

}

else if(i>index){

//3. increase every container div top after current by 40;

var newTop = currentTop+increaseby;
band.setBandShiftAndWidth(newTop, currentHeight);

   }

}


}


Now, we need to two more properties into Timeline._Band class to keep track of the current band's height and provide two functions to get these properties.

Timeline._Band = function(timeline, bandInfo, index) {

this._timeline = timeline;

....

/* We need to add these two properties to keep track of the height of the current "band" */

 

 

this._bandHeight = 0 ;

 

this._bandTop = 0;

...

}

Timeline._Band.prototype.getBandHeight = function()

{

return this._bandHeight;

}

Timeline._Band.prototype.getBandTop = function()

{

return this._bandTop;

}

Remember in TimeLine class, we had the distributeWidthUponEvent() function? Well, to call that, we need to get the current "band" a way to get a reference to TimeLine class. let's do that.

Timeline._Band.prototype.getTimeline = function()

{

      return this._timeline;

}

Next, if you remember the pseudo code my friend Jackson mentioned, step 3), we need to increase the height of every band after the current band, so how do we keep track of the index?

Timeline._Band.prototype.getIndex = function()

{

return this._index ;

}

At last, where exactly is TimeLine project setting the style.top and style.height of a band? It's in "

Timeline._Band.prototype.setBandShiftAndWidth 

function.   Remember, I added two properties previously to keep track the height and top of the current band, well, this is where I'm going to set it.

Timeline._Band.prototype.setBandShiftAndWidth = function(shift, width) {

if (this._timeline.isHorizontal()) {

 

this._bandTop = shift;

this._bandHeight = width;

 

this._div.style.top = shift + "px";

this._div.style.height = width + "px";

} else {

this._div.style.left = shift + "px";

this._div.style.width = width + "px";

}

};

 

2.   Paninter.js

Now let's get to the fun part, where we add events to our timeline.  This logic is as we add event, we check it's height (which is measured in "em") and if it exceed the current "height" of the band, then we need to call ._distributeWidthsUponEvent() function in step 1), so it'll adjust the timeline.

Now, first at a instance of the current band to a local variable for convience's sake

Timeline.DurationEventPainter.prototype.paint = function() {

...

var currentBand = this._band;

...

}

Second,  go to the function where Event is being added,  get the height of the current band, convert "em" to px by times itself by "11.4" (my estimate), if it exceeds the height of the current band, get the TimeLine object and have it readjust the timeline bands based on the current band index.

var createEventDiv = function(evt, highlightIndex) {

var startDate = evt.getStart();

var endDate = evt.getEnd();

 

var startPixel = Math.round(p._band.dateToPixelOffset(startDate));

var endPixel = Math.round(p._band.dateToPixelOffset(endDate));

 

var streamOffset = (trackOffset +

p._layout.getTrack(evt) * (trackHeight + trackGap));

 

if (evt.isInstant()) {

/* Get the height of the current Band */

var bandheight = currentBand.getBandHeight();

/* If the converted em/px is greater than the height of the current band */

if(streamOffset*11.4>bandheight)

{

             /* Get the timeline and re-adjust the height of all bands after the current band */

currentBand.getTimeline()._distributeWidthsUponEvent(currentBand.getIndex());

}

 

createInstantDiv(evt, startPixel, endPixel, streamOffset + "em",

highlightIndex, streamOffset - trackGap, trackHeight + 2 * trackGap);

} else {

createDurationDiv(evt, startPixel, endPixel, streamOffset + "em",

highlightIndex, streamOffset - trackGap, trackHeight + 2 * trackGap);

}

};


Hope I didn't forget anything. In the next post, I'll break apart the timeline javascript files and explain how it works in more detail.
 
Anonymous comments are disabled