February 11, 2010

Visualize your SharePoint Calendar with a Dynamic Timeline

[Update Feb 2010: see new blog post for  more advanced version]
I’m a lazy developer. I hate reinventing the wheel. Worse, I like stealing other people’s wheels! I guess that’s why I love JavaScript so much: no need to have access to any server, I can embed my script into this very blog (or into a Content Editor Web Part if I’m using SharePoint). I can point to a JavaScript library other geniuses have invented, and use their fancy functions against some of the enterprise data accessible through XML (RSS, Web services, REST, etc.), and voila! I can impress my boss in just an hour of work! In future articles, I’m going to show a few examples of amazing things you can do in JavaScript and enterprise data.
Starting with this example. Here I’m going to show how to visualize a SharePoint calendar with the SIMILE Timeline widget.
 Timeline
This is what we’re going to use:
  • SIMILE Timeline widget for the visualization.
  • Darren’s SharePoint JavaScript API to access any SharePoint data. Any SharePoint list or document library data can be accessed through a web service, so Darren’s API makes accessing SharePoint data really easy (you can also use JQuery as I will show in future articles, although I like this one better). You will soon find yourself using this API in many scripts, so to improve its availability I suggest you copy Darren’s JavaScript files on your production server.
  • And finally SharePoint calendar data itself. As I said, the events details are accessible in JavaScript through the calendar web service. To make life easier, all you need to provide is the RSS feed associated to your calendar (see bottom of the script). The feed URL contains both the path of your calendar and its list ID, which is all we need to access the web service.
So there you have it. Modify just the few lines where you see “myserver” (i.e. line 7, 8 9 and bottom of the script), insert the script into a Content Editor Web Part, and enjoy!

<script language="javascript" type="text/javascript">
var Timeline_urlPrefix = "http://simile.mit.edu/timeline/api/";
// Include these javascript files only if not loaded already by another web part
// In portals like a SharePoint page, you never know what script might already be loaded by the master page or other web parts
includeJSScript("http://simile.mit.edu/timeline/api/timeline-api.js");
includeJSScript("http://myserver/js/spapi/spapi_core.js");
includeJSScript("http://myserver/js/spapi/spapi_types.js");
includeJSScript("http://myserver/js/spapi/spapi_lists.js");
function includeJSScript(p_file) {
// before we insert this script, we need to check if it already exists
var bAlreadyExists = false;
var scripts = document.getElementsByTagName('script');
for (var i = 0; i < scripts.length; i++) {
if (scripts[i].src == p_file) {
//scripts[i] is the one
bAlreadyExists = true;
break;
}
}
if (!bAlreadyExists) {
var v_script = document.createElement('script');
v_script.type = 'text/javascript';
v_script.src = p_file;
document.getElementsByTagName('head')[0].appendChild(v_script);
}
}
// Call the web service associated to the calendar to extract its items
function getCalendarListItems() 
{ 
var lists = new SPAPI_Lists(ExtractWebSiteURL(MyList_RSS_Url)); 
var items = lists.getListItems(ExtractListID(MyList_RSS_Url),'',"<Query><OrderBy><FieldRef Name='EventDate' /></OrderBy></Query>",'',100); 
if (items.status == 200) 
{ 
var rows = items.responseXML.getElementsByTagName("z:row");        
return rows; 
} 
else 
{ 
return null; 
} 
} 
var resizeTimerID = null; 
function formatDateString(strDate) 
{ 
var yearStr = strDate.substr(0, 4); 
var monthStr = strDate.substr(5, 2); 
var dayStr = strDate.substr(8, 2);        
return monthStr + "/" + dayStr + "/" + yearStr + " " + strDate.substr(11); 
} 
// This is one of the most important functions. Once the calendar events are retrieved, 
// we loop through them and add them to the timeline
function main() 
{ 
var items = getCalendarListItems(); 
if (items == null) 
{ 
document.getElementById("my-timeline").innerHTML = "Cannot get items from list <i>" + ExtractWebSiteURL(MyList_RSS_Url) + "/" + ExtractListID(MyList_RSS_Url) + "<i>"; 
return; 
} 
var eventSource = new Timeline.DefaultEventSource(); 
// for each event returned by the calednar web service, we create a timeline event
for (var i = 0; i < items.length; ++i) 
{ 
var ows_EventDate = formatDateString(items[i].getAttribute("ows_EventDate")); 
var ows_EndDate = formatDateString(items[i].getAttribute("ows_EndDate")); 
var ows_Title = items[i].getAttribute("ows_Title"); 
var ows_Location = items[i].getAttribute("ows_Location");
var eventDate = new Date(ows_EventDate); 
var endDate = new Date(ows_EndDate); 
var event = new Timeline.DefaultEventSource.Event( 
eventDate, //start 
endDate, //end 
eventDate, //latestStart 
endDate , //earliestEnd 
true, //instant (use FALSE if events are longer than a few hours of duration
ows_Title, //text 
"<strong>Where? </strong>" + ows_Location + "<br><strong>When? </strong>"  //description that appears in a bubble when user clicks on the event
); 
eventSource.add(event); 
} 
// This is where we define 3 timelines. Advanced users can play with these parameters to use different timeline or timeline behaviors
// See http://code.google.com/p/simile-widgets/wiki/Timeline for more information
var theme = Timeline.ClassicTheme.create(); // create the theme    
theme.event.bubble.width = 300;   // modify this bubble size to fit your needs   
theme.event.bubble.height = 170;    
var bandInfos = [ 
Timeline.createBandInfo({ 
trackGap:       0.5, 
width:          "60%", 
intervalUnit:   Timeline.DateTime.WEEK, 
intervalPixels: 100, 
timeZone : 8, 
eventSource: eventSource, theme:theme 
}), 
Timeline.createBandInfo({ 
showEventText:  false, 
trackHeight:    0.5, 
trackGap:       0.2, 
width:          "25%", 
intervalUnit:   Timeline.DateTime.MONTH, 
intervalPixels: 150, 
timeZone : 8, 
eventSource: eventSource 
}), 
Timeline.createBandInfo({ 
showEventText:  false, 
trackHeight:    0.5, 
trackGap:       0.2, 
width:          "15%", 
intervalUnit:   Timeline.DateTime.YEAR, 
intervalPixels: 400, 
timeZone : 8, 
eventSource: eventSource 
}) 
]; 
bandInfos[1].syncWith = 0; 
bandInfos[2].highlight = true; 
bandInfos[2].syncWith = 1; 
var timeLine = Timeline.create(document.getElementById("my-timeline"), bandInfos); 
} 
function ExtractWebSiteURL(sUrl) {
var index = sUrl.toLowerCase().indexOf("_layouts");
var MyCurrentPath = "";
if (index != -1) {
MyCurrentPath = sUrl.substring(0, index);
MyCurrentPath = MyCurrentPath.substring(0,MyCurrentPath.lastIndexOf('/'));
}
else { return null;}
return MyCurrentPath 
}
function ExtractListID(sUrl) {
var index = sUrl.toLowerCase().indexOf("_layouts");
var DestinationListID = "";
if (index != -1) {
DestinationListID = unescape(sUrl.substring(index + 28, sUrl.length));
}
else { return null;}
return DestinationListID 
}
// _spBodyOnLoadFunctionNames.push is a SharePoint function that insures that the script will be run only AFTER the page has been loaded
_spBodyOnLoadFunctionNames.push("main"); 
// ******************** Settings ******************** 
// URl of the RSS associated to your calendarvar
MyList_RSS_Url = "http://myserver/mysite/_layouts/listfeed.aspx?List=%7B3186664F%2D626C%2D4925%2D896B%2D53517E1D0244%7D"; 
</script>
<!-- Feel free to modify the following parameters: height, border, font -->
<div id="my-timeline" style="height: 120px; border: 1px solid #aaa; font-size: 9pt"></div>  

37 comments:

  1. Herve - thanks for this post; would love to be using it, but when I set it up it throws "Timeline is undefined" at:

    var eventSource = new Timeline.DefaultEventSource();
    // for each event returned by the calednar web service, we create a timeline event

    ...any thoughts?

    ReplyDelete
  2. This typically happens if the timeline javascript file has not been loaded, or has not finished loading. Make sure that:
    1) you correctly load the javascript:
    includeJSScript("http://simile.mit.edu/timeline/api/timeline-api.js");

    or
    <script type="text/javascript" src="http://simile.mit.edu/timeline/api/timeline-api.js"></script>

    2) the function where you have "new Timeline.DefaultEventSource();" is called only after the DOM model has been loaded. In SharePoint, it means using _spBodyOnLoadFunctionNames.push("main");
    If you're using something else that SharePoint, then maybe use jquery $(document).ready() function.

    ReplyDelete
  3. thanks for posting this. i've got a similar approach to coding and thanks to you & google i can impress my boss too ;)
    jk we just deployed sharepoint and i think this visual approach to a project overview will help our PHB's

    ReplyDelete
  4. Thanks for the feedback. I promised myself I would post more of these nice JQuery tips and tricks. I'll try to catch up soon, I promise!

    ReplyDelete
  5. I've been working for a month trying to get this working in Sharepoint 2010 but am having trouble debugging the code properly, starting with _spBodyOnLoadFunctionNames.push("main"). I think the replacement function is ExecuteOrDelayUntilScriptLoaded, but I'm not sure how to code it properly. If anyone has successfully adapted this code it would be awesome to make a timeline out of any Sharepoint list. Thanks Herve! -R

    ReplyDelete
  6. I've stopped using "_spBodyOnLoadFunctionName" a while ago (though it should work on SharePoint 2007 and 2010). I'm now exclusively using JQuery. So instead of:
    _spBodyOnLoadFunctionNames.push("main");
    function main() {
    ...
    }

    do this
    - load your jquery library. Example: *<*script src="code.jquery.com/jquery-latest.min.js" type="text/javascript">*

    - instead of the code above, replace the "main" function with:


    $(function() {
    /// whatever you had in the main function before
    alert("Can you see this message once the page content is loaded?");
    });

    Try this, then let me know where you're stuck.

    ReplyDelete
  7. Excellent Posting.

    But I'm getting javascript error "'SPAPI_Lists' is undefined" and the webpart is blank. I have added all the relevant JS files into the document library and mentioned the path correctly in the code.
    But still getting the error.

    If I "Edit Page", the webpart showing the timeline but I "exit Edit Mode" again the issue exists.

    Any ideas!!!!!!!!!!

    ReplyDelete
  8. Thank you for sharing this. Just wanted to ask, this works fine in IE7 and in Mozilla. However, for Safari and Chrome, the items.length is always equal to zero. Anyone encountered this?

    ReplyDelete
  9. The link to Darren Johnstone's site seems to be down. Do you know where it would be possible to get a copy of his SharePoint Javascript API

    ReplyDelete
  10. Thanks, this is great! Do you have any advice for displaying events over time?

    ReplyDelete
  11. I receive the script error Access is denied for the spapi_core.js file.

    Anyone encountered this?

    ReplyDelete
    Replies
    1. You want to download these libraries on your own domain.

      Delete
  12. I can't get to Darren's API information either. Has any been able to find this?

    ReplyDelete
  13. You can find Darrens API here:
    http://zer0c00l.in/wiki/index.php?title=SharePoint_SPAPI_HowTO

    ReplyDelete
  14. I'm having a problems getting this to display in IE9. All I get is a blank box. in Firefox it works fine.
    I found this - http://drupal.org/node/79472
    Anyone else had this problem and knows a way around to fix in SharePoint2010

    ReplyDelete
    Replies
    1. I've been off this project for a while now, but on some instances I found that there might be a race issues, where the timeline loads after (instead of before) the DOM has loaded. Try loading the timeline script higher on your page. Also try using the JQuery $.ready() instead of SharePoint _spBodyOnLoadFunctionNames.push function.
      Let me know if that helped.

      Delete
    2. Thanks for the reply Herve.
      Also you wouldn't happen to know how to display the calendar body column in the bubble? I've tried ows_Description, ows_Summary and ows_Body - non of which work

      Delete
    3. ps. loading the timeline script higher on the page seems to work better - thanks for the tip

      Delete
    4. I don't have access to a sharepoint server this weekend, but for finding out the name of a column, I use one of the following:
      - got to the calendar (or list) settings, and select the column in the list of columns. The URL contains the column name, but you need to add "ows_" (well, not always but generally yes)
      - use this tool http://sharepointsearchserv.codeplex.com/
      - or this on http://www.codeplex.com/MOSSSearchCoder

      Delete
  15. This comment has been removed by the author.

    ReplyDelete
  16. This seems to work with the jQuery $.ready()... but I still needed to put a time delay:
    ~~~~~~~~~
    $(document).ready(function(){
    setTimeout(main, 1500);
    });
    ~~~~~~~~~
    However....I'm not able to use my own custom theme. I get a "'Timeline' is undefined" error.

    ReplyDelete
  17. Hi Herve,
    Nice one :). Is it possible to show the description column of a list in a window somethng similar to "http://www.webresourcesource.com/awesome-jquery-timeline-plugin/"

    ReplyDelete
  18. @mudasar: You can add anything you want in the variable "ows_Title". The name of the variable is misleading, I should have named it sBubbleContent. In this "bubble", you can add content from a description column, a date, etc.
    Example: if you want to have the item title + description + date:
    var ows_Title = items[i].getAttribute("ows_Title") + "
    " + items[i].getAttribute("ows_Description") + "
    " + items[i].getAttribute("ows_Date");
    (assuming your columns are named "Description" and "Date")

    ReplyDelete
  19. Another trick: sometimes you don't want to load all the bubbles with all the content at the page load, for performance reasons. Instead you want to load the bubble content only when user clicks on the dot or the icon on the time line. You can hijack the click even and fill in the related bubble by adding a code like this after the "Timeline.create":
    var timeLine = Timeline.create(document.getElementById("my-timeline"), bandInfos);


    // hijack popup window callback to show my custom content

    Timeline.DurationEventPainter.prototype._showBubble = function(x, y, evt) {

    var div = this._band.openBubbleForPoint(x, y,

    this._theme.event.bubble.width,

    this._theme.event.bubble.height

    );

    evt.fillInfoBubble(div, this._theme, this._band.getLabeller());

    // make sure you have added a DIV with id "idBubbleContent" when you iterated through your list items
    $("#idBubbleContent").html("my on-demand HTML content goes here");

    ReplyDelete
    Replies
    1. Thanks a lot for the reply mate. It works superbly now.One additional thing i need to do is to change the bubble based on due date. Like for example if the due date is crossed and status field column is still "IN PROGRESS" bubble should turn red. Is this possible?

      Delete
    2. And Mate one more issue is,When we drag the days timeline band, The month and year band are not synchornized with it. I mean to say all doesn't get dragged togeather.

      Delete
    3. For your sync issue, make sure you use:

      bandInfos[band to synchronize].syncWith = band to synchronize with;

      example:

      bandInfos[1].syncWith = 0;

      Remember that the first band is 0.

      Delete
  20. Hi Herve.

    Thanks for the code! Just what I needed. However, when I insert the script into a CEWP no timeline is showed. Instead, the CEWP shows the text of the script I inserted. Any ideas? I'm new to scripting (it was unceremoniously dumped on my lap) so I've no clue how to resolve this.

    Thanks!

    ReplyDelete
  21. @Nicolodean - that's probably because you're using the "Text Editor" button. Use "Source Editor" instead. Better, put your script in a file that you will name "MyCalendarScript.js", and save that js under a Sharepoint document libary. Now copy the url of this new file, and paste it in the field "Content Link" of your CEWP settings.

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
  22. Hi herve,
    If the code is added to a webpart zone which is below some of the webpart may be at the end. Onclick of event icon, the bubble position moves out of the screen pane. Not getting how to solve this issue. If the position of the bubble is not adjustable can u just tell me what changed i need to make to show the data in tooltip atleast instead of bubble.

    ReplyDelete
    Replies
    1. Hello Mudasar,
      I didn't have to deal with this problem to be honest. But maybe this blog article is good start: http://josh-in-antarctica.blogspot.com/2008/03/customizing-timeline-event-bubbles.html

      Delete
  23. This comment has been removed by the author.

    ReplyDelete
  24. Hi all,
    I have developed a super cool timeline webpart integrated with sharepoint task list. You can integrate it with sharepoint pretty easily. If you require a quick solution. Please contact me on michealforus@googlemail.com

    ReplyDelete
  25. Great tool.
    small question, which file contains the When? and Where? for the bubble? my boss asking to remove the question mark. Thanks.

    ReplyDelete
    Replies
    1. found it..it is in the above script. thanks great work.

      Delete
  26. Is there anyway I can use multiple sharepoint calendars as the data source(s)?

    ReplyDelete