Categories
projects software engineering

Introducing chronoline.js, a JavaScript library for timelines

Sadly, a lot of my work at Zanbato is behind closed doors, but recently, I have been working on a widget that is definitely not proprietary and is available for all of you to use, extend, or even just look at: chronoline.js.

chronoline.js is a library for making a chronology timeline out of events on a horizontal timescale. From a list of dates and events, it can generate a graphical representation of schedules, historical events, deadlines, and more.

This is just the short pitch, but hop on over to http://stoicloofah.github.com/chronoline.js/ where you can see a few examples. They don’t quite demonstrate what I think is quite a bit of built-in flexibility, but definitely let me know what you think!

51 replies on “Introducing chronoline.js, a JavaScript library for timelines”

Nice! Wondering how it compares with SIMILE Timeline: http://www.simile-widgets.org/timeline/

I’ve used SIMILE once & found it to be very comprehensive. What it misses though is interactive editing of the events & dates. For example, you cannot add an event in the UI or drag the end date of an event!

Thanks for the comment, Venkat! I saw SIMILE before I started working on this, and yeah, it’s sweet. Our graphic designer wanted a very specific look, though, and anticipating an arbitrarily long list of enhancements to make, I figured it would be easier to build it myself.

I filed your suggestion in github https://github.com/StoicLoofah/chronoline.js/issues/2 . No promises on being able to do this. Having figured out how to put events onto the timeline, I now see why SIMILE also didn’t do it. The best that can be done right now is changing the events list in JS and reinitializing the widget with new data. I’ll see if I can figure out a good in-between solution.

Hi, love your chronoline – but is it me or I am missing something but seems for me and also in the demo pages – if you compare date coding to what shows in browser the Event dates are one month out of sync??

Glad you like it, Colin! As for the dates, I think this might be the oddity that JavaScript months start at 0 instead of 1 (whereas years and days both correlate correctly). Is that what you’re talking about?

Gotcha. So because javascript months start at 0, June is actually 5, not 7. Don’t ask me why they implemented it that way: I was confused too. So just take the month number and subtract 1 to get what it is in JS.

As for defaulting to today, if you don’t pass in a default start date, it should automatically go to today.

Excellent work. Loving it.
I have a draft build ready to launch but couldn’t get qtip to work in IE. I was using ver1. Upgraded to qtip2 – still no tool tips in IE. I just revisited your site
http://stoicloofah.github.io/chronoline.js/
and tested in IE9. ahhhh, no tips.
I will still implement but hope that I [we] can resolve this.
thanks

Yeah, I wasn’t able to get qtip working in IE, which I think was because of how it implements SVGs. If you checkout line 409, I actually forbid qtips in IE. Have you tried IE9 with that line changed? I’m curious about whether it would work now.

I doubt Colin is still following but for others…
>> to begin at the current date [php]
$now = mktime(….) – (44*24*60*60);
this is 1 month to adjust for array[0] and a 1/2 month so as to center

defaultStartDate (… echo date(“Y, n, j”, $now);)

&&!jQuery.browser.msie
I don’t have other IE versions to test but that did it for 9.! Funny, I spotted that line a while back and never thought to check that there was a corresponding call for ie
Question is… how does it behave in other versions of IE? I don’t mind if tips don’t work but a crash (on the home page) is NOT so good 🙂

I will implement and see how the world responds.
Glad that you are still with this. Nice – tidy and compact.

So I’m playing around with this on my computer, and I can’t get the tooltips working in IE9 even after removing the browser check. Were there any other changes you made to it to make it work?

Just thought I would log my live version:
http://bodhinyanarama.net.nz/

Small question…
How to add the day letter next to the day number. I figure it is in the strftime [around lines 50 + 147] but I am not sure how to add %a? I would see how this looks and maybe try for a substr ‘Su’ ‘Mo’ – or even S M T…

Yup, that’s exactly where to look. You will need to add more functionality to the formatDate method since I only implemented a subset of the functionality of a strftime function. It would probably be

if(formatString.indexOf(‘%a’) != -1)
ret = ret.replace(‘%a’, dayNames[date.getDay()].charAt(0));

You would also need to write out the dayNames array as well. Then, swap 147 for a new format string.

thanks for the quick replies

I added:
var dayNames = [‘Sun’,’Mon’,’Tue’,’Wed’,’Thu’,’Fri’,’Sat’];
just above function formatDate()
inserted: [with {}]
if(formatString.indexOf(‘%a’) != -1) {
ret = ret.replace(‘%a’, dayNames[date.getDay()].charAt(0)); }
just after %d {} declaratation

what is now line 152 I changed to:
labelFormat: ‘%a’,
no change

There are 3 instances of formatDate()
// subSublabels. These can float
formatDate(curDate, ‘%Y’)
// special markers for today
formatDate(curDate, ‘%b’)
// sublabels. These can float
formatDate(curDate, ‘%b’)

I don’t see where %d is written.
_____
aside:
if(dateNum.length < 2)
dateNum = '0' + dateNum;
makes no diff. as you are already using %d which pulls the leading zero. For safety?

I search %d and there are only 2 instances – both in formatDate()
what am I missing?

lunch 🙂

A bit more head scratching… 🙂
Drop the: charAt(0) in
dayNames[date.getDay()].charAt(0)); and adjust using the format of dayNames[]
There is only one ref. to: labelFormat: [152] so it seems irrelevant.?
It took a while to nail it but the day# is written in: t.drawLabelsHelper [515?]

var day = curDate.getDate()+’ ‘+formatDate(curDate,’%a’);

Wow, I can’t believe it works [grin]
Delete that last comment – mutterings; thx

>> labelFormat for labels
Yes, you bypass the whole %d around [533]
var displayDate = String(day);
For consistency this would be good to use. Perhaps with the leading zero as an option?

Setting the day
I eventually noticed that I had lost the month display under the day numbers.
if(day == 1 && t.subLabel == ‘month’) [559]
Because string for ‘day’ is now more than numerical we need:
if(day.substring(0, 2) == 1
assuming there is a space between the number and the day-letter – so as not to have 010 011 etc. I have the working ver. live.

I suggest dropping – substring(0, 3); in
monthNames[date.getMonth()].substring(0, 3);
monthNames is essentially a user variable and the choice of strlen should be open. Also less work for the script 🙂

If you really have the space-time I would appreciate any guidance on separately formatting the day letter. I tried things but it is getting into Rapael which I have no time for. I thought to font-size – 1 and maybe fill change. This is icing on my happy cake.

Great work Kevin. I am enjoying exploring.

Alright, I fixed a lot of the things mentioned in https://github.com/StoicLoofah/chronoline.js/commit/6a573f8d5c1254acf90db5cbeb753b36296fa291

  1. refactored formatDate so that it’s a Date method and added strftime
  2. the labelFormat should be used now. If you want to put the day of the week in there, you can use the format string. Since that’s localized to the display, it avoids the month label issue that you described
  3. the substring on month is there because that’s how %b is defined in strftime. For the full month display, we could add %B as an option as well. Let me know if you would be interested in that
  4. For formatting the day, there’s the fontAttrs option on chronoline that does that formatting. If you want to change the formatting for that specifically, you’ll need to write in new properties around line 542 label.attr({‘font-size’: 10, fill: ‘#000000’});. The properties available are http://raphaeljs.com/reference.html#Element.attr

Let me know if you have any other feedback!

>> fixed a lot of the things…
I see this has been updated on github. Cool. I will download and test a bit. Glad that this is a live project. Thanks.

>> substring on month
My point was that monthNames is defined. If we drop the substr then we write ‘January’ etc. If the user wants ‘Jan’ then they can modify the array as they wish.

>> 4.
Thanks for the Raph. link. I will see how far I can get. As I say, it will be icing on the cake. I may not have the time.

Icing + cherry.!
Basically no problem. Define dayAttrs:{} then around [544] split day value into number and letter then apply fontAttrs and dayAttrs to each with adjusted x, Y offsets. Bingo!

With several offset adjustments (number, letter, today box) it seemed easiest to drop the month shown under today; using labelBox. It is also a bit crowded with number+letter

>> substring on month
There are also instances of toUpperCase()
The year is numeric. The month, both length and case, I feel is best left as a user choice.

BTW – did you look at qtip 2.1.1?

I think I have arrived at a point of completion. A huge thanks for all your help.

Center today: Is there a way to run {timeline1.goToToday();} onload?
The best I have so far is: defaultStartDate: new Date(Date.now()-11e8), [11e8=half visibleSpan]
Is there a way to js insert the value of visibleSpan?

Afer some user feedback I am now using today: line with stroke-width:6 which obscures events, both visually and tip-hover. I am not using sections so can use .toBack() but, for those using sections, I wonder if there is a better fix to write the today-line behind events?
To amplify the today-line I added triangles top and bottom. My code:
triangleAttrs: { fill: “#B2AF7E”, stroke: “#B2AF7E”, “stroke-width”: 1 },
addTriangles: true,
I put it inside… if(t.markToday == ‘line’) { … }
if (t.addTriangles) {
var p=t.paper.path(“M”+(o-10)+”,0 l10,16 10,-16 z”).attr(t.triangleAttrs); // upper triangle
var q=t.paper.path(“M”+(o-16)+”,”+(N+0)+” l32,0 -16,8 z”).attr(t.triangleAttrs); // lc ‘l’ = relative
}

General thoughts…
I would suggest updating index.html event data, esp. so there is a today to go to.

Rapael syntax: .attr instances. EG.
var line = t.paper.path(‘M’ + x + ‘,0L’ + x + ‘,’ + dateLineY);
line.attr(t.todayAttrs);
becomes: var line = t.paper.path(‘M’ + x + ‘,0L’ + x + ‘,’ + dateLineY).attr(t.todayAttrs);
Less code always seems better to me.

cheers…

  1. Yes, the right way to center on today is to pass in Date.now(). You can grab the default value of visibleSpan as Chronoline.defaults.visibleSpan. Alternatively, you can invoke t.goToToday() immediately after creation
  2. Using toBack is probably the right fix for the today line
  3. Thanks for the code about the triangles
  4. Good point about the events data being pretty out of date. I’ll file a ticket to get more current data
  5. There are a lot of places where I could slide the attrs onto the same line as the initialization. I don’t have really strong feelings on this either way; I probably implemented it this way because I was unaware of this feature at the time (thanks for pointing it out!), but looking at the documentation, the varying return type makes this use a bit less obvious

1. defaultStartDate: new Date(Date.now()-11e8),
Sorry, can’t get it.
() -(Chronoline.defaults.visibleSpan)/2),
My js is generally a struggle :o)
2. toBack – yes, works for me but the line is lost when using sections.
5. I generally write tight code – as in few line breaks, concatenation when possible – assuming it is faster. I have never tested this assumption 🙂

Dang… trying options for visSpan and realised I forgot to ask HOW to invoke t.goToToday()
struggle – yeah? 😮

Invoking goToToday is just that easy.

var t = new Chronoline({...});
t.goToToday();

I see what you’re doing with the today line now. Uh, I don’t have a solution for that offhand; I would need to reorder where I draw that line so it comes after the sections but before the events. You could probably pull off the same effect as well.

struggle…
var t = new Chronoline({…});
t.goToToday();
and the ‘…’ = ?
I tried defaults.visibleSpan
played with: dom, events, options..
even tried …
doh.!

>> reorder where I draw that line
or send section-band to back?
I confess I have no personal need here so won’t explore further.

Fill in the … with however your’e currently creating your Chronoline in javascript. The improtant thing is that you assign the chrono to a variable (in this case, t), and then you can call the goToToday function on the chronoline you just created

sorry – but I really can’t see what to put in …there…
Chrono is already assigned to a var., ie. timeline1 [in index.html] Using: timeline1.goToToday(); doesn’t work. I tried a few other (semi)inspired guesses.

struggle -_-

I think you may have to give me specific code; say, in relation to the index example?
end of the week and all’s well
thx

No, you’re doing it right; it would just be timeline1.goToToday();, so I don’t know why it isn’t working. If you look at line 133 in the index, that’s all I’m doing. It just happens to be that I attached it to a button click event instead of just calling it directly.

I don’t want to press you too far on this. Basically I am happy enough with defaultStartDate: new Date(Date.now()-11e8),
Partly I don’t like to quit and partly I would suggest that goToToday be the default – with a comment noting manual setting syntax. But, can you make it work?

I have had some positive user feedback on my homepage setup. thx

I’ll consider it. I guess it really depends on what the use case of the timeline is, but in either case, it really is just meant to be a configurable default as you used it.

Is There a way to replace tooltip with proposer bootstrap like. I have html text Andre in tooltip it´s not good. Qtip dont work i dont see tooltip

I tried to replace qTip with bootstrap popover to get the information show on click and stay as i want.
Is there a simple way to do that ?

Thank’s for your help this the code that i write and it’s work fine

$node.attr('rel','popover');
$node.attr('data-original-title',title);
$node.attr('data-content',description);
$node.popover({placement:'auto',trigger:'manual',html:true,container: 'body'});

Hello,

Its a very nice script to work with timelines.

I need your help, as I want to display timline-item-title along with lines (instead on mouse hover effect), can you help me, how I can do this?
I search a lot but couldn’t get the exact issues/blogs.

As I put a question yesterday, regarding that I got a solution.

If you want to display your timeline title inline with timelines, then, you have to do little changes as following

In your instantiation of chroneline give some height to event with attribute
eventHeight: 10/11/12 (this is because our text can displayed well in the timeline)

In Chroneline.js please at // drawing events for loop, after the following line
elem = t.paper.rect(startX, upperY, width, t.eventHeight).attr(t.eventAttrs);
add
var elemtext = t.paper.text(startX + 10, upperY + 5, myEvent.mytitle)
elemtext.attr(‘text-anchor’, ‘start’);
elemtext.attr(‘font-weight’, ‘bold’);
NOTE: here myEvent.mytitle, where “mytitle” is custom attribute declared by me at the time of building events data array(because tooltip uses standard “title” attribute which also have some html code meanwhile for inline title I just want to display title text only).

So, a text element will be added on timeline.
Hope this would help other who want to display timline-item-title with lines.

Also this can be added globaly in chroneline.js with few more settings like display title inline and timline-title-attributes (events).

Thanks….

Thanks for dropping the code here, Ashvin. That is indeed the right place to drop this code. There might be some additional adjusting you need to do for items that are stacked on top of each other, so let me know if you run into any weird positioning issues.

Hi, I wonder if you could help me with a problem, to explore a database, I can not show all results in a single Chronoline graph, with the code that I have now I can only show the last date of the query. add the code that I use to know if there is any solution. Hope your answer.

$(document).ready(function(){

var ini=new Date();

anio= ini.getFullYear() + ‘,’ + ini.getMonth() + ‘,’ + ini.getDate();
var events = [
{dates: [new Date(anio)], title: “””},

];

//Propiedades de la linea de tiempo
var timeline1 = new Chronoline(document.getElementById(“target1”), events,
{animated: true,
tooltips: true,
defaultStartDate: new Date(2014, 0, 1),
//sections: sections,
sectionLabelAttrs: {‘fill’: ‘#997e3d’, ‘font-weight’: ‘bold’},
scrollLeft: prevMonth,
scrollRight: nextMonth,
markToday: ‘labelBox’,
draggable: true
});
});

sorry, the above code was incomplete, this is which I use

$(document).ready(function(){

var ini=new Date();

anio= ini.getFullYear() + ‘,’ + ini.getMonth() + ‘,’ + ini.getDate();
var events = [
{dates: [new Date(anio)], title: “””},

];

//Propiedades de la linea de tiempo
var timeline1 = new Chronoline(document.getElementById(“target1”), events,
{animated: true,
tooltips: true,
defaultStartDate: new Date(2014, 0, 1),
//sections: sections,
sectionLabelAttrs: {‘fill’: ‘#997e3d’, ‘font-weight’: ‘bold’},
scrollLeft: prevMonth,
scrollRight: nextMonth,
markToday: ‘labelBox’,
draggable: true
});
});

Sorry, I’m not sure I understand the problem that you are having. I only see 1 event on 1 that you’re actually plugging into chronoline. Were you expecting to see more events on the timeline?

i had customized the js and now i need to disable saturday and sunday from click event and even for holidays (the days are fetched from database)

Hmm, the logic of the script largely assumes that the narrowest resolution that you want is days. I guess it’s conceivable that you could use this as a base to do hours and minutes, but that would probably be a significant fork from what it currently is.

Hey Kevin, thanks for the great product! I’m not sure if you’re still following (this post is 3 years old 😛 )

I did, on my own, figure out a way to add additional events from UI without reloading the entire timeline (I think). However, there are certain event placement issues on the timeline. For example, subsequent addition of events from UI make them appear on the timeline at increasing heights, even if there’s no event previously on that date.

Basically, I introduced a function “drawEvent” which takes an event as an input and adds it into to event array and draws it. Fundamentally, I just copy pasted the relevant code into “drawEvent” with minor modifications to avoid redrawing the entire timeline:

function drawEvent(newevent){

//************************************************************************
// need to toss the time variance bits
for(var i = 0; i < events.length; i++){
for(var j = 0; j < events[i].dates.length; j++){
events[i].dates[j] = new Date(events[i].dates[j].getTime());
events[i].dates[j].stripTime();
}
}
t.events = events;
t.events.sort(t.sortEvents);

for(var i = 0; i < t.events.length; i++){
var found = false;
if(events[i] == newevent)
{
var startPx = t.msToPx(t.events[i].dates[0].getTime()) – t.circleRadius;
console.log("Current i-event = "+events[i].title);
for(var j = 0; j < t.eventRows.length; j++){

console.log("for j = "+j+", t.rowLastPxs[j] = "+t.rowLastPxs[j]+" & startPx = "+startPx);
if(t.rowLastPxs[j] < startPx){
t.eventRows[j].push(t.events[i]);
console.log("Pushed "+i+" "+events[i].title);
t.rowLastPxs[j] = t.msToPx(getEndDate(t.events[i].dates).getTime()) + t.circleRadius;
found = true;
break;
}
}
if(!found){
t.eventRows.push([t.events[i]]);
console.log("Pushed2 "+i+" "+events[i].title);
t.rowLastPxs.push(t.msToPx(getEndDate(t.events[i].dates).getTime()) + t.circleRadius);
}
}
}

for(var row = 0; row < t.eventRows.length; row++){
var upperY = t.totalHeight – t.dateLabelHeight – (row + 1) * (t.eventMargin + t.eventHeight);
for(var col = 0; col < t.eventRows[row].length; col++){
var myEvent = t.eventRows[row][col];
if(myEvent == newevent)
{
var startX = (myEvent.dates[0].getTime() – t.startTime) * t.pxRatio;
console.log("startX = "+startX);
var elem = null;
if(myEvent.dates.length == 1){ // it's a single point
elem = t.paper.circle(startX, upperY + t.circleRadius, t.circleRadius).attr(t.eventAttrs);
}
else { // it's a range
var width = (getEndDate(myEvent.dates) – myEvent.dates[0]) * t.pxRatio;
// left rounded corner
var leftCircle = t.paper.circle(startX, upperY + t.circleRadius, t.circleRadius).attr(t.eventAttrs);
if(typeof myEvent.attrs != "undefined"){
leftCircle.attr(myEvent.attrs);
}
addElemClass(t.paperType, leftCircle.node, 'chronoline-event');
// right rounded corner
var rightCircle = t.paper.circle(startX + width, upperY + t.circleRadius, t.circleRadius).attr(t.eventAttrs);
if(typeof myEvent.attrs != "undefined"){
rightCircle.attr(myEvent.attrs);
}
addElemClass(t.paperType, rightCircle.node, 'chronoline-event');
elem = t.paper.rect(startX, upperY, width, t.eventHeight).attr(t.eventAttrs);
}

if(typeof myEvent.link != 'undefined') {
elem.data('link', myEvent.link);
elem.click(function(){
window.location.href = this.data('link');
});
}

if(typeof myEvent.attrs != "undefined"){
elem.attr(myEvent.attrs);
}
addElemClass(t.paperType, elem.node, 'chronoline-event');

elem.attr('title', myEvent.title);
if(t.tooltips){
var description = myEvent.description;
var title = myEvent.title;
if(typeof description == "undefined" || description === ''){
description = title;
title = '';
}
var $node = jQuery(elem.node);
if(Raphael.type == 'SVG')
$node = $node.parent();
$node.qtip({
content: {
title: title,
text: description
},
position: {
my: 'top left',
target: 'mouse',
viewport: jQuery(window), // Keep it on-screen at all times if possible
adjust: {
x: 10, y: 10
}
},
hide: {
fixed: true // Helps to prevent the tooltip from hiding ocassionally when tracking!
},
style: {
classes: 'qtip-shadow qtip-dark qtip-rounded'
}
});
}
if(t.sections !== null && t.sectionLabelsOnHover){
// some magic here to tie the event back to the section label element
var originalIndex = myEvent.section;
if(typeof originalIndex != "undefined"){
var newIndex = 0;
for(var i = 0; i < t.sections.length; i++){
if(t.sections[i].section == originalIndex){
elem.data('sectionLabel', t.sectionLabelSet[i]);
break;
}
}
elem.hover(function(){this.data('sectionLabel').animate({opacity: 1}, 200);},
function(){this.data('sectionLabel').animate({opacity: 0}, 200);});
}
}
}
}
}

Thanks for your help!

Leave a Reply

Your email address will not be published. Required fields are marked *