cal_heatmap_rails 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- /*! cal-heatmap v3.0.9 (Thu Aug 01 2013 18:58:29)
1
+ /*! cal-heatmap v3.1.0 (Thu Aug 08 2013 01:26:26)
2
2
  * ---------------------------------------------
3
3
  * Cal-Heatmap is a javascript module to create calendar heatmap to visualize time series data, a la github contribution graph
4
4
  * https://github.com/kamisama/cal-heatmap
@@ -198,7 +198,7 @@ var CalHeatMap = function() {
198
198
 
199
199
 
200
200
  // ================================================
201
- // CALLBACK
201
+ // EVENTS CALLBACK
202
202
  // ================================================
203
203
 
204
204
  // Callback when clicking on a time block
@@ -208,10 +208,10 @@ var CalHeatMap = function() {
208
208
  afterLoad : null,
209
209
 
210
210
  // Callback after loading the next domain in the calendar
211
- afterLoadNextDomain : function(start) {},
211
+ afterLoadNextDomain : null,
212
212
 
213
213
  // Callback after loading the previous domain in the calendar
214
- afterLoadPreviousDomain : function(start) {},
214
+ afterLoadPreviousDomain : null,
215
215
 
216
216
  // Callback after finishing all actions on the calendar
217
217
  onComplete : null,
@@ -228,7 +228,7 @@ var CalHeatMap = function() {
228
228
  //
229
229
  // This callback is also executed once, after calling previous(),
230
230
  // only when the max domain is reached
231
- onMaxDomainReached: function(reached) {},
231
+ onMaxDomainReached: null,
232
232
 
233
233
  // Callback triggered after calling previous().
234
234
  // The `status` argument is equal to true if there is no
@@ -236,17 +236,16 @@ var CalHeatMap = function() {
236
236
  //
237
237
  // This callback is also executed once, after calling next(),
238
238
  // only when the min domain is reached
239
- onMinDomainReached: function(reached) {}
239
+ onMinDomainReached: null
240
240
  };
241
241
 
242
242
 
243
-
244
243
  this._domainType = {
245
244
  "min" : {
246
245
  name: "minute",
247
246
  level: 10,
248
- row: function(d) {return 10;},
249
- column: function(d) { return 6; },
247
+ row: function() {return 10;},
248
+ column: function() { return 6; },
250
249
  position: {
251
250
  x : function(d) { return Math.floor(d.getMinutes() / self._domainType.min.row(d)); },
252
251
  y : function(d) { return d.getMinutes() % self._domainType.min.row(d);}
@@ -256,12 +255,15 @@ var CalHeatMap = function() {
256
255
  legend: "",
257
256
  connector: "at"
258
257
  },
259
- extractUnit : function(d) { return d.getMinutes(); }
258
+ extractUnit : function(d) {
259
+ var dt = new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes());
260
+ return dt.getTime();
261
+ }
260
262
  },
261
263
  "hour" : {
262
264
  name: "hour",
263
265
  level: 20,
264
- row: function(d) {return 6;},
266
+ row: function() {return 6;},
265
267
  column: function(d) {
266
268
  switch(self.options.domain) {
267
269
  case "day" : return 4;
@@ -286,14 +288,14 @@ var CalHeatMap = function() {
286
288
  connector: "at"
287
289
  },
288
290
  extractUnit : function(d) {
289
- var formatHour = d3.time.format("%H");
290
- return d.getFullYear() + "" + self.getDayOfYear(d) + "" + formatHour(d);
291
+ var dt = new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours());
292
+ return dt.getTime();
291
293
  }
292
294
  },
293
295
  "day" : {
294
296
  name: "day",
295
297
  level: 30,
296
- row: function(d) {return 7;},
298
+ row: function() {return 7;},
297
299
  column: function(d) {
298
300
  d = new Date(d);
299
301
  switch(self.options.domain) {
@@ -322,12 +324,15 @@ var CalHeatMap = function() {
322
324
  legend: "%e %b",
323
325
  connector: "on"
324
326
  },
325
- extractUnit : function(d) { return d.getFullYear() + "" + self.getDayOfYear(d); }
327
+ extractUnit : function(d) {
328
+ var dt = new Date(d.getFullYear(), d.getMonth(), d.getDate());
329
+ return dt.getTime();
330
+ }
326
331
  },
327
332
  "week" : {
328
333
  name: "week",
329
334
  level: 40,
330
- row: function(d) {return 1;},
335
+ row: function() {return 1;},
331
336
  column: function(d) {
332
337
  d = new Date(d);
333
338
  switch(self.options.domain) {
@@ -343,7 +348,7 @@ var CalHeatMap = function() {
343
348
  case "month" : return self.getWeekNumber(d) - self.getWeekNumber(new Date(d.getFullYear(), d.getMonth())) - 1;
344
349
  }
345
350
  },
346
- y: function(d) {
351
+ y: function() {
347
352
  return 0;
348
353
  }
349
354
  },
@@ -352,13 +357,22 @@ var CalHeatMap = function() {
352
357
  legend: "%B Week #%W",
353
358
  connector: "on"
354
359
  },
355
- extractUnit : function(d) { return self.getWeekNumber(d); }
360
+ extractUnit : function(d) {
361
+ var dt = new Date(d.getFullYear(), d.getMonth(), d.getDate());
362
+ // According to ISO-8601, week number computation are based on week starting on Monday
363
+ var weekDay = dt.getDay()-1;
364
+ if (weekDay < 0) {
365
+ weekDay = 6;
366
+ }
367
+ dt.setDate(dt.getDate() - weekDay);
368
+ return dt.getTime();
369
+ }
356
370
  },
357
371
  "month" : {
358
372
  name: "month",
359
373
  level: 50,
360
- row: function(d) {return 1;},
361
- column: function(d) {return 12;},
374
+ row: function() {return 1;},
375
+ column: function() {return 12;},
362
376
  position: {
363
377
  x : function(d) { return Math.floor(d.getMonth() / self._domainType.month.row(d)); },
364
378
  y : function(d) { return d.getMonth() % self._domainType.month.row(d);}
@@ -368,13 +382,16 @@ var CalHeatMap = function() {
368
382
  legend: "%B",
369
383
  connector: "on"
370
384
  },
371
- extractUnit : function(d) { return d.getMonth(); }
385
+ extractUnit : function(d) {
386
+ var dt = new Date(d.getFullYear(), d.getMonth());
387
+ return dt.getTime();
388
+ }
372
389
  },
373
390
  "year" : {
374
391
  name: "year",
375
392
  level: 60,
376
- row: function(d) {return 1;},
377
- column: function(d) {return 12;},
393
+ row: function() {return 1;},
394
+ column: function() {return 12;},
378
395
  position: {
379
396
  x : function(d) { return Math.floor(d.getFullYear() / this._domainType.year.row(d)); },
380
397
  y : function(d) { return d.getFullYear() % this._domainType.year.row(d);}
@@ -384,7 +401,10 @@ var CalHeatMap = function() {
384
401
  legend: "%Y",
385
402
  connector: "on"
386
403
  },
387
- extractUnit : function(d) { return d.getFullYear(); }
404
+ extractUnit : function(d) {
405
+ var dt = new Date(d.getFullYear());
406
+ return dt.getTime();
407
+ }
388
408
  }
389
409
  };
390
410
 
@@ -423,7 +443,7 @@ var CalHeatMap = function() {
423
443
 
424
444
  // Record all the valid domains
425
445
  // Each domain value is a timestamp in milliseconds
426
- this._domains = [];
446
+ this._domains = d3.map();
427
447
 
428
448
  var graphDim = {
429
449
  width: 0,
@@ -433,6 +453,11 @@ var CalHeatMap = function() {
433
453
  this.NAVIGATE_LEFT = 1;
434
454
  this.NAVIGATE_RIGHT = 2;
435
455
 
456
+ // Various update mode when using the update() API
457
+ this.RESET_ALL_ON_UPDATE = 0;
458
+ this.RESET_SINGLE_ON_UPDATE = 1;
459
+ this.APPEND_ON_UPDATE = 2;
460
+
436
461
  this.root = null;
437
462
 
438
463
  this._maxDomainReached = false;
@@ -446,7 +471,9 @@ var CalHeatMap = function() {
446
471
  */
447
472
  function _init() {
448
473
 
449
- self._domains = self.getDomain(self.options.start).map(function(d) { return d.getTime(); });
474
+ self.getDomain(self.options.start).map(function(d) { return d.getTime(); }).map(function(d) {
475
+ self._domains.set(d, self.getSubDomain(d).map(function(d) { return {t: self._domainType[self.options.subDomain].extractUnit(d), v: null}; }));
476
+ });
450
477
 
451
478
  self.root = d3.select(self.options.itemSelector);
452
479
 
@@ -454,20 +481,35 @@ var CalHeatMap = function() {
454
481
 
455
482
  if (self.options.paintOnLoad) {
456
483
 
484
+ self.verticalDomainLabel = (self.options.label.position === "top" || self.options.label.position === "bottom");
485
+
486
+ self.domainVerticalLabelHeight = Math.max(25, self.options.cellSize*2);
487
+ self.domainHorizontalLabelWidth = 0;
488
+
489
+ if (!self.verticalDomainLabel) {
490
+ self.domainVerticalLabelHeight = 0;
491
+ self.domainHorizontalLabelWidth = self.options.label.width;
492
+ }
493
+
494
+ // @todo : check validity
495
+ if (typeof self.options.domainMargin === "number") {
496
+ self.options.domainMargin = [self.options.domainMargin, self.options.domainMargin, self.options.domainMargin, self.options.domainMargin];
497
+ }
498
+
457
499
  self.paint();
458
500
 
459
501
  // =========================================================================//
460
502
  // ATTACHING DOMAIN NAVIGATION EVENT //
461
503
  // =========================================================================//
462
504
  if (self.options.nextSelector !== false) {
463
- d3.select(self.options.nextSelector).on("click." + self.options.itemNamespace, function(d) {
505
+ d3.select(self.options.nextSelector).on("click." + self.options.itemNamespace, function() {
464
506
  d3.event.preventDefault();
465
507
  return self.loadNextDomain();
466
508
  });
467
509
  }
468
510
 
469
511
  if (self.options.previousSelector !== false) {
470
- d3.select(self.options.previousSelector).on("click." + self.options.itemNamespace, function(d) {
512
+ d3.select(self.options.previousSelector).on("click." + self.options.itemNamespace, function() {
471
513
  d3.event.preventDefault();
472
514
  return self.loadPreviousDomain();
473
515
  });
@@ -484,12 +526,14 @@ var CalHeatMap = function() {
484
526
 
485
527
  // Fill the graph with some datas
486
528
  if (self.options.loadOnInit) {
529
+ var domains = self._domains.keys().sort();
487
530
  self.getDatas(
488
531
  self.options.data,
489
- new Date(self._domains[0]),
490
- self.getSubDomain(self._domains[self._domains.length-1]).pop(),
491
- function(data) {
492
- self.fill(data, self.svg);
532
+ new Date(parseInt(domains[0], 10)),
533
+ self.getSubDomain(parseInt(domains[domains.length-1], 10)).pop(),
534
+ function() {
535
+ self.fill();
536
+ self.onComplete();
493
537
  }
494
538
  );
495
539
  } else {
@@ -500,6 +544,25 @@ var CalHeatMap = function() {
500
544
  return true;
501
545
  }
502
546
 
547
+ // Return the width of the domain block, without the domain gutter
548
+ // @param int d Domain start timestamp
549
+ function w(d, outer) {
550
+ var width = self.options.cellSize*self._domainType[self.options.subDomain].column(d) + self.options.cellPadding*self._domainType[self.options.subDomain].column(d);
551
+ if (typeof outer !== "undefined" && outer === true) {
552
+ return width += self.domainHorizontalLabelWidth + self.options.domainGutter + self.options.domainMargin[1] + self.options.domainMargin[3];
553
+ }
554
+ return width;
555
+ }
556
+
557
+ // Return the height of the domain block, without the domain gutter
558
+ function h(d, outer) {
559
+ var height = self.options.cellSize*self._domainType[self.options.subDomain].row(d) + self.options.cellPadding*self._domainType[self.options.subDomain].row(d);
560
+ if (typeof outer !== "undefined" && outer === true) {
561
+ height += self.options.domainGutter + self.domainVerticalLabelHeight + self.options.domainMargin[0] + self.options.domainMargin[2];
562
+ }
563
+ return height;
564
+ }
565
+
503
566
 
504
567
  /**
505
568
  *
@@ -512,44 +575,10 @@ var CalHeatMap = function() {
512
575
  navigationDir = false;
513
576
  }
514
577
 
515
- var verticalDomainLabel = (self.options.label.position === "top" || self.options.label.position === "bottom");
516
-
517
- var domainVerticalLabelHeight = Math.max(25, self.options.cellSize*2);
518
- var domainHorizontalLabelWidth = 0;
519
-
520
- if (!verticalDomainLabel) {
521
- domainVerticalLabelHeight = 0;
522
- domainHorizontalLabelWidth = self.options.label.width;
523
- }
524
-
525
- // @todo : check validity
526
- if (typeof self.options.domainMargin === "number") {
527
- self.options.domainMargin = [self.options.domainMargin, self.options.domainMargin, self.options.domainMargin, self.options.domainMargin];
528
- }
529
-
530
- // Return the width of the domain block, without the domain gutter
531
- // @param int d Domain start timestamp
532
- var w = function(d, outer) {
533
- var width = self.options.cellSize*self._domainType[self.options.subDomain].column(d) + self.options.cellPadding*self._domainType[self.options.subDomain].column(d);
534
- if (typeof outer !== "undefined" && outer === true) {
535
- return width += domainHorizontalLabelWidth + self.options.domainGutter + self.options.domainMargin[1] + self.options.domainMargin[3];
536
- }
537
- return width;
538
- };
539
-
540
- // Return the height of the domain block, without the domain gutter
541
- var h = function(d, outer) {
542
- var height = self.options.cellSize*self._domainType[self.options.subDomain].row(d) + self.options.cellPadding*self._domainType[self.options.subDomain].row(d);
543
- if (typeof outer !== "undefined" && outer === true) {
544
- height += self.options.domainGutter + domainVerticalLabelHeight + self.options.domainMargin[0] + self.options.domainMargin[2];
545
- }
546
- return height;
547
- };
548
-
549
578
  // Painting all the domains
550
579
  var domainSvg = self.root.select(".graph")
551
580
  .selectAll(".graph-domain")
552
- .data(self._domains, function(d) { return d;})
581
+ .data(self._domains.keys().map(function(d) { return parseInt(d, 10); }), function(d) { return d;})
553
582
  ;
554
583
 
555
584
  var enteringDomainDim = 0;
@@ -560,26 +589,26 @@ var CalHeatMap = function() {
560
589
  // PAINTING DOMAIN //
561
590
  // =========================================================================//
562
591
 
563
- var svg = domainSvg
592
+ self.svg = domainSvg
564
593
  .enter()
565
594
  .append("svg")
566
- .attr("width", function(d, i){
595
+ .attr("width", function(d) {
567
596
  return w(d, true);
568
597
  })
569
598
  .attr("height", function(d) {
570
599
  return h(d, true);
571
600
  })
572
- .attr("x", function(d, i) {
601
+ .attr("x", function(d) {
573
602
  if (self.options.verticalOrientation) {
574
603
  graphDim.width = w(d, true);
575
604
  return 0;
576
605
  } else {
577
- return getDomainPosition(i, graphDim, "width", w(d, true));
606
+ return getDomainPosition(d, graphDim, "width", w(d, true));
578
607
  }
579
608
  })
580
- .attr("y", function(d, i) {
609
+ .attr("y", function(d) {
581
610
  if (self.options.verticalOrientation) {
582
- return getDomainPosition(i, graphDim, "height", h(d, true));
611
+ return getDomainPosition(d, graphDim, "height", h(d, true));
583
612
  } else {
584
613
  graphDim.height = h(d, true);
585
614
  return 0;
@@ -590,34 +619,38 @@ var CalHeatMap = function() {
590
619
  var date = new Date(d);
591
620
  switch(self.options.domain) {
592
621
  case "hour" : classname += " h_" + date.getHours();
622
+ /* falls through */
593
623
  case "day" : classname += " d_" + date.getDate() + " dy_" + date.getDay();
624
+ /* falls through */
594
625
  case "week" : classname += " w_" + self.getWeekNumber(date);
626
+ /* falls through */
595
627
  case "month" : classname += " m_" + (date.getMonth() + 1);
628
+ /* falls through */
596
629
  case "year" : classname += " y_" + date.getFullYear();
597
630
  }
598
631
  return classname;
599
632
  })
600
633
  ;
601
634
 
602
- function getDomainPosition(index, graphDim, axis, domainDim) {
635
+ function getDomainPosition(domainIndex, graphDim, axis, domainDim) {
603
636
  var tmp = 0;
604
637
  switch(navigationDir) {
605
638
  case false :
606
- if (index > 0) {
639
+ //if (domainIndex > 0) {
607
640
  tmp = graphDim[axis];
608
- }
641
+ //}
609
642
 
610
643
  graphDim[axis] += domainDim;
611
- self.domainPosition.pushPosition(tmp);
644
+ self.domainPosition.setPosition(domainIndex, tmp);
612
645
  return tmp;
613
646
 
614
647
  case self.NAVIGATE_RIGHT :
615
- self.domainPosition.pushPosition(graphDim[axis]);
648
+ self.domainPosition.setPosition(domainIndex, graphDim[axis]);
616
649
 
617
650
  enteringDomainDim = domainDim;
618
- exitingDomainDim = self.domainPosition.getPosition(1);
651
+ exitingDomainDim = self.domainPosition.getPositionFromIndex(1);
619
652
 
620
- self.domainPosition.shiftRight(exitingDomainDim);
653
+ self.domainPosition.shiftRightBy(exitingDomainDim);
621
654
  return graphDim[axis];
622
655
 
623
656
  case self.NAVIGATE_LEFT :
@@ -626,40 +659,42 @@ var CalHeatMap = function() {
626
659
  enteringDomainDim = -tmp;
627
660
  exitingDomainDim = graphDim[axis] - self.domainPosition.getLast();
628
661
 
629
- self.domainPosition.unshiftPosition(tmp);
630
- self.domainPosition.shiftLeft(enteringDomainDim);
662
+ self.domainPosition.setPosition(domainIndex, tmp);
663
+ self.domainPosition.shiftLeftBy(enteringDomainDim);
631
664
  return tmp;
632
665
  }
633
666
  }
634
667
 
635
- svg.append("rect")
636
- .attr("width", function(d, i) { return w(d, true) - self.options.domainGutter - self.options.cellPadding; })
637
- .attr("height", function(d, i) { return h(d, true) - self.options.domainGutter - self.options.cellPadding; })
668
+ self.svg.append("rect")
669
+ .attr("width", function(d) { return w(d, true) - self.options.domainGutter - self.options.cellPadding; })
670
+ .attr("height", function(d) { return h(d, true) - self.options.domainGutter - self.options.cellPadding; })
638
671
  .attr("class", "domain-background")
639
- ;
672
+ ;
640
673
 
641
674
  // =========================================================================//
642
675
  // PAINTING SUBDOMAINS //
643
676
  // =========================================================================//
644
- var subDomainSvgGroup = svg.append("svg")
645
- .attr("x", function(d, i) {
646
- switch(self.options.label.position) {
647
- case "left" : return domainHorizontalLabelWidth + self.options.domainMargin[3];
648
- default : return self.options.domainMargin[3];
677
+ var subDomainSvgGroup = self.svg.append("svg")
678
+ .attr("x", function() {
679
+ if (self.options.label.position === "left") {
680
+ return self.domainHorizontalLabelWidth + self.options.domainMargin[3];
681
+ } else {
682
+ return self.options.domainMargin[3];
649
683
  }
650
684
  })
651
- .attr("y", function(d, i) {
652
- switch(self.options.label.position) {
653
- case "top" : return domainVerticalLabelHeight + self.options.domainMargin[0];
654
- default : return self.options.domainMargin[0];
685
+ .attr("y", function() {
686
+ if (self.options.label.position === "top") {
687
+ return self.domainVerticalLabelHeight + self.options.domainMargin[0];
688
+ } else {
689
+ return self.options.domainMargin[0];
655
690
  }
656
691
  })
657
692
  .attr("class", "graph-subdomain-group")
658
693
  ;
659
694
 
660
695
  var rect = subDomainSvgGroup
661
- .selectAll("svg")
662
- .data(function(d) { return self.getSubDomain(d); })
696
+ .selectAll("g")
697
+ .data(function(d) { return self._domains.get(d); }, function(d) { return d.t; })
663
698
  .enter()
664
699
  .append("g")
665
700
  ;
@@ -667,41 +702,41 @@ var CalHeatMap = function() {
667
702
  rect
668
703
  .append("rect")
669
704
  .attr("class", function(d) {
670
- return "graph-rect" + self.getHighlightClassName(d) + (self.options.onClick !== null ? " hover_cursor" : "");
705
+ return "graph-rect" + self.getHighlightClassName(d.t) + (self.options.onClick !== null ? " hover_cursor" : "");
671
706
  })
672
707
  .attr("width", self.options.cellSize)
673
708
  .attr("height", self.options.cellSize)
674
- .attr("x", function(d) { return self.positionSubDomainX(d); })
675
- .attr("y", function(d) { return self.positionSubDomainY(d); })
709
+ .attr("x", function(d) { return self.positionSubDomainX(d.t); })
710
+ .attr("y", function(d) { return self.positionSubDomainY(d.t); })
676
711
  .on("click", function(d) {
677
712
  if (self.options.onClick !== null) {
678
- return self.onClick(d, null);
713
+ return self.onClick(new Date(d.t), d.v);
714
+ }
715
+ })
716
+ .call(function(selection) {
717
+ if (self.options.cellRadius > 0) {
718
+ selection
719
+ .attr("rx", self.options.cellRadius)
720
+ .attr("ry", self.options.cellRadius)
721
+ ;
679
722
  }
680
723
  })
681
- .call(radius)
682
724
  ;
683
725
 
684
- function radius(selection) {
685
- if (self.options.cellRadius > 0) {
686
- selection
687
- .attr("rx", self.options.cellRadius)
688
- .attr("ry", self.options.cellRadius)
689
- ;
690
- }
691
- }
692
-
726
+ // Appending a title to each subdomain
727
+ rect.append("title").text(function(d){ return self.formatDate(new Date(d.t), self.options.subDomainDateFormat); });
693
728
 
694
729
 
695
730
  // =========================================================================//
696
731
  // PAINTING LABEL //
697
732
  // =========================================================================//
698
- svg.append("text")
733
+ self.svg.append("text")
699
734
  .attr("class", "graph-label")
700
- .attr("y", function(d, i) {
735
+ .attr("y", function(d) {
701
736
  var y = self.options.domainMargin[0];
702
737
  switch(self.options.label.position) {
703
- case "top" : y += domainVerticalLabelHeight/2; break;
704
- case "bottom" : y += h(d) + domainVerticalLabelHeight/2;
738
+ case "top" : y += self.domainVerticalLabelHeight/2; break;
739
+ case "bottom" : y += h(d) + self.domainVerticalLabelHeight/2;
705
740
  }
706
741
 
707
742
  return y + self.options.label.offset.y *
@@ -711,7 +746,7 @@ var CalHeatMap = function() {
711
746
  -1 : 1
712
747
  );
713
748
  })
714
- .attr("x", function(d, i){
749
+ .attr("x", function(d){
715
750
  var x = self.options.domainMargin[3];
716
751
  switch(self.options.label.position) {
717
752
  case "right" : x += w(d); break;
@@ -720,7 +755,7 @@ var CalHeatMap = function() {
720
755
  }
721
756
 
722
757
  if (self.options.label.align === "right") {
723
- return x + domainHorizontalLabelWidth - self.options.label.offset.x *
758
+ return x + self.domainHorizontalLabelWidth - self.options.label.offset.x *
724
759
  (self.options.label.rotate === "right" ? -1 : 1);
725
760
  }
726
761
  return x + self.options.label.offset.x;
@@ -735,8 +770,8 @@ var CalHeatMap = function() {
735
770
  default : return "middle";
736
771
  }
737
772
  })
738
- .attr("dominant-baseline", function() { return verticalDomainLabel ? "middle" : "top"; })
739
- .text(function(d, i) { return self.formatDate(new Date(self._domains[i]), self.options.domainLabelFormat); })
773
+ .attr("dominant-baseline", function() { return self.verticalDomainLabel ? "middle" : "top"; })
774
+ .text(function(d) { return self.formatDate(new Date(d), self.options.domainLabelFormat); })
740
775
  .call(domainRotate)
741
776
  ;
742
777
 
@@ -748,7 +783,7 @@ var CalHeatMap = function() {
748
783
  var s = "rotate(90), ";
749
784
  switch(self.options.label.position) {
750
785
  case "right" : s += "translate(-" + w(d) + " , -" + w(d) + ")"; break;
751
- case "left" : s += "translate(0, -" + domainHorizontalLabelWidth + ")"; break;
786
+ case "left" : s += "translate(0, -" + self.domainHorizontalLabelWidth + ")"; break;
752
787
  }
753
788
 
754
789
  return s;
@@ -759,8 +794,8 @@ var CalHeatMap = function() {
759
794
  .attr("transform", function(d) {
760
795
  var s = "rotate(270), ";
761
796
  switch(self.options.label.position) {
762
- case "right" : s += "translate(-" + (w(d) + domainHorizontalLabelWidth) + " , " + w(d) + ")"; break;
763
- case "left" : s += "translate(-" + (domainHorizontalLabelWidth) + " , " + domainHorizontalLabelWidth + ")"; break;
797
+ case "right" : s += "translate(-" + (w(d) + self.domainHorizontalLabelWidth) + " , " + w(d) + ")"; break;
798
+ case "left" : s += "translate(-" + (self.domainHorizontalLabelWidth) + " , " + self.domainHorizontalLabelWidth + ")"; break;
764
799
  }
765
800
 
766
801
  return s;
@@ -770,8 +805,6 @@ var CalHeatMap = function() {
770
805
  }
771
806
 
772
807
 
773
- // Appending a title to each subdomain
774
- rect.append("title").text(function(d){ return self.formatDate(d, self.options.subDomainDateFormat); });
775
808
 
776
809
 
777
810
  // =========================================================================//
@@ -780,12 +813,12 @@ var CalHeatMap = function() {
780
813
  if (self.options.subDomainTextFormat !== null) {
781
814
  rect
782
815
  .append("text")
783
- .attr("class", function(d) { return "subdomain-text" + self.getHighlightClassName(d); })
784
- .attr("x", function(d) { return self.positionSubDomainX(d) + self.options.cellSize/2; })
785
- .attr("y", function(d) { return self.positionSubDomainY(d) + self.options.cellSize/2; })
816
+ .attr("class", function(d) { return "subdomain-text" + self.getHighlightClassName(d.t); })
817
+ .attr("x", function(d) { return self.positionSubDomainX(d.t) + self.options.cellSize/2; })
818
+ .attr("y", function(d) { return self.positionSubDomainY(d.t) + self.options.cellSize/2; })
786
819
  .attr("text-anchor", "middle")
787
820
  .attr("dominant-baseline", "central")
788
- .text(function(d){ return self.formatDate(d, self.options.subDomainTextFormat); })
821
+ .text(function(d){ return self.formatDate(new Date(d.t), self.options.subDomainTextFormat); })
789
822
  ;
790
823
  }
791
824
 
@@ -795,19 +828,11 @@ var CalHeatMap = function() {
795
828
 
796
829
  if (navigationDir !== false) {
797
830
  domainSvg.transition().duration(self.options.animationDuration)
798
- .attr("x", function(d, i){
799
- if (self.options.verticalOrientation) {
800
- return 0;
801
- } else {
802
- return self.domainPosition.getPosition(i);
803
- }
831
+ .attr("x", function(d){
832
+ return self.options.verticalOrientation ? 0 : self.domainPosition.getPosition(d);
804
833
  })
805
- .attr("y", function(d, i){
806
- if (self.options.verticalOrientation) {
807
- return self.domainPosition.getPosition(i);
808
- } else {
809
- return 0;
810
- }
834
+ .attr("y", function(d){
835
+ return self.options.verticalOrientation? self.domainPosition.getPosition(d) : 0;
811
836
  })
812
837
  ;
813
838
  }
@@ -823,7 +848,7 @@ var CalHeatMap = function() {
823
848
 
824
849
  // At the time of exit, domainsWidth and domainsHeight already automatically shifted
825
850
  domainSvg.exit().transition().duration(self.options.animationDuration)
826
- .attr("x", function(d, i){
851
+ .attr("x", function(d){
827
852
  if (self.options.verticalOrientation) {
828
853
  return 0;
829
854
  } else {
@@ -851,13 +876,56 @@ var CalHeatMap = function() {
851
876
  .attr("width", function() { return graphDim.width - self.options.domainGutter - self.options.cellPadding; })
852
877
  .attr("height", function() { return graphDim.height - self.options.domainGutter - self.options.cellPadding; })
853
878
  ;
879
+ };
854
880
 
855
- if (self.svg === null) {
856
- self.svg = svg;
857
- } else {
858
- self.svg = self.root.select(".graph").selectAll("svg")
859
- .data(self._domains, function(d) {return d;});
860
- }
881
+ this.fill = function() {
882
+ var rect = self.svg
883
+ .selectAll("svg").selectAll("g")
884
+ .data(function(d) { return self._domains.get(d); }, function(d) { return d.t; })
885
+ ;
886
+
887
+ rect.transition().select("rect")
888
+ .attr("class", function(d) {
889
+
890
+ var htmlClass = "graph-rect" + self.getHighlightClassName(d.t);
891
+
892
+ if (d.v !== null) {
893
+ htmlClass += " " + self.legend(d.v);
894
+ } else if (self.options.considerMissingDataAsZero) {
895
+ htmlClass += " " + self.legend(0);
896
+ }
897
+
898
+ if (self.options.onClick !== null) {
899
+ htmlClass += " hover_cursor";
900
+ }
901
+
902
+ return htmlClass;
903
+ })
904
+ ;
905
+
906
+
907
+ rect.transition().select("title")
908
+ .text(function(d) {
909
+
910
+ if (d.v === null && !self.options.considerMissingDataAsZero) {
911
+ return (self.options.subDomainTitleFormat.empty).format({
912
+ date: self.formatDate(new Date(d.t), self.options.subDomainDateFormat)
913
+ });
914
+ } else {
915
+ var value = d.v;
916
+ // Consider null as 0
917
+ if (value === null && self.options.considerMissingDataAsZero) {
918
+ value = 0;
919
+ }
920
+
921
+ return (self.options.subDomainTitleFormat.filled).format({
922
+ count: self.formatNumber(value),
923
+ name: self.options.itemName[(value !== 1 ? 1 : 0)],
924
+ connector: self._domainType[self.options.subDomain].format.connector,
925
+ date: self.formatDate(new Date(d.t), self.options.subDomainDateFormat)
926
+ });
927
+ }
928
+ });
861
929
  };
862
930
 
863
931
 
@@ -1014,104 +1082,113 @@ var CalHeatMap = function() {
1014
1082
 
1015
1083
  CalHeatMap.prototype = {
1016
1084
 
1017
-
1018
1085
  // =========================================================================//
1019
- // CALLBACK //
1086
+ // EVENTS CALLBACK //
1020
1087
  // =========================================================================//
1021
1088
 
1022
1089
  /**
1023
- * Callback when clicking on a subdomain cell
1024
- * @param Date d Date of the subdomain block
1025
- * @param int itemNb Number of items in that date
1090
+ * Helper method for triggering event callback
1091
+ *
1092
+ * @param string eventName Name of the event to trigger
1093
+ * @param array successArgs List of argument to pass to the callback
1094
+ * @param boolean skip Whether to skip the event triggering
1095
+ * @return mixed True when the triggering was skipped, false on error, else the callback function
1026
1096
  */
1027
- onClick : function(d, itemNb) {
1028
- if (typeof this.options.onClick === "function") {
1029
- return this.options.onClick(d, itemNb);
1097
+ triggerEvent: function(eventName, successArgs, skip) {
1098
+ if ((arguments.length === 3 && skip) || this.options[eventName] === null) {
1099
+ return true;
1100
+ }
1101
+
1102
+ if (typeof this.options[eventName] === "function") {
1103
+ if (typeof successArgs === "function") {
1104
+ successArgs = successArgs();
1105
+ }
1106
+ return this.options[eventName].apply(this, successArgs);
1030
1107
  } else {
1031
- console.log("Provided callback for onClick is not a function.");
1108
+ console.log("Provided callback for " + eventName + " is not a function.");
1032
1109
  return false;
1033
1110
  }
1034
1111
  },
1035
1112
 
1036
1113
  /**
1037
- * Callback to fire after drawing the calendar, but before filling it
1114
+ * Event triggered on a mouse click on a subDomain cell
1115
+ *
1116
+ * @param Date d Date of the subdomain block
1117
+ * @param int itemNb Number of items in that date
1118
+ */
1119
+ onClick : function(d, itemNb) {
1120
+ return this.triggerEvent("onClick", [d, itemNb]);
1121
+ },
1122
+
1123
+ /**
1124
+ * Event triggered after drawing the calendar, byt before filling it with data
1038
1125
  */
1039
1126
  afterLoad : function() {
1040
- if (typeof (this.options.afterLoad) === "function") {
1041
- return this.options.afterLoad();
1042
- } else {
1043
- console.log("Provided callback for afterLoad is not a function.");
1044
- return false;
1045
- }
1127
+ return this.triggerEvent("afterLoad");
1046
1128
  },
1047
1129
 
1048
1130
  /**
1049
- * Callback to fire at the end, when all actions on the calendar are completed
1131
+ * Event triggered after completing drawing and filling the calendar
1050
1132
  */
1051
1133
  onComplete : function() {
1052
- if (this.options.onComplete === null || this._completed === true) {
1053
- return true;
1054
- }
1055
-
1134
+ var response = this.triggerEvent("onComplete", [], this._completed);
1056
1135
  this._completed = true;
1057
- if (typeof (this.options.onComplete) === "function") {
1058
- return this.options.onComplete();
1059
- } else {
1060
- console.log("Provided callback for onComplete is not a function.");
1061
- return false;
1062
- }
1136
+ return response;
1063
1137
  },
1064
1138
 
1065
1139
  /**
1066
- * Callback after shifting the calendar one domain back
1140
+ * Event triggered after shifting the calendar one domain back
1141
+ *
1067
1142
  * @param Date start Domain start date
1068
1143
  * @param Date end Domain end date
1069
1144
  */
1070
1145
  afterLoadPreviousDomain: function(start) {
1071
- if (typeof (this.options.afterLoadPreviousDomain) === "function") {
1072
- var subDomain = this.getSubDomain(start);
1073
- return this.options.afterLoadPreviousDomain(subDomain.shift(), subDomain.pop());
1074
- } else {
1075
- console.log("Provided callback for afterLoadPreviousDomain is not a function.");
1076
- return false;
1077
- }
1146
+ var parent = this;
1147
+ return this.triggerEvent("afterLoadPreviousDomain", function() {
1148
+ var subDomain = parent.getSubDomain(start);
1149
+ return [subDomain.shift(), subDomain.pop()];
1150
+ });
1078
1151
  },
1079
1152
 
1080
1153
  /**
1081
- * Callback after shifting the calendar one domain above
1154
+ * Event triggered after shifting the calendar one domain above
1155
+ *
1082
1156
  * @param Date start Domain start date
1083
1157
  * @param Date end Domain end date
1084
1158
  */
1085
1159
  afterLoadNextDomain: function(start) {
1086
- if (typeof (this.options.afterLoadNextDomain) === "function") {
1087
- var subDomain = this.getSubDomain(start);
1088
- return this.options.afterLoadNextDomain(subDomain.shift(), subDomain.pop());
1089
- } else {
1090
- console.log("Provided callback for afterLoadNextDomain is not a function.");
1091
- return false;
1092
- }
1160
+ var parent = this;
1161
+ return this.triggerEvent("afterLoadNextDomain", function() {
1162
+ var subDomain = parent.getSubDomain(start);
1163
+ return [subDomain.shift(), subDomain.pop()];
1164
+ });
1093
1165
  },
1094
1166
 
1167
+ /**
1168
+ * Event triggered after loading the leftmost domain allowed by minDate
1169
+ *
1170
+ * @param boolean reached True if the leftmost domain was reached
1171
+ */
1095
1172
  onMinDomainReached: function(reached) {
1096
1173
  this._minDomainReached = reached;
1097
- if (typeof (this.options.onMinDomainReached) === "function") {
1098
- return this.options.onMinDomainReached(reached);
1099
- } else {
1100
- console.log("Provided callback for onMinDomainReached is not a function.");
1101
- return false;
1102
- }
1174
+ return this.triggerEvent("onMinDomainReached", [reached]);
1103
1175
  },
1104
1176
 
1177
+ /**
1178
+ * Event triggered after loading the rightmost domain allowed by maxDate
1179
+ *
1180
+ * @param boolean reached True if the rightmost domain was reached
1181
+ */
1105
1182
  onMaxDomainReached: function(reached) {
1106
1183
  this._maxDomainReached = reached;
1107
- if (typeof (this.options.onMaxDomainReached) === "function") {
1108
- return this.options.onMaxDomainReached(reached);
1109
- } else {
1110
- console.log("Provided callback for onMaxDomainReached is not a function.");
1111
- return false;
1112
- }
1184
+ return this.triggerEvent("onMaxDomainReached", [reached]);
1113
1185
  },
1114
1186
 
1187
+
1188
+ // =========================================================================//
1189
+ // FORMATTER //
1190
+ // =========================================================================//
1191
+
1115
1192
  formatNumber: d3.format(",g"),
1116
1193
 
1117
1194
  formatDate: function(d, format) {
@@ -1127,6 +1204,7 @@ CalHeatMap.prototype = {
1127
1204
  }
1128
1205
  },
1129
1206
 
1207
+
1130
1208
  // =========================================================================//
1131
1209
  // DOMAIN NAVIGATION //
1132
1210
  // =========================================================================//
@@ -1147,28 +1225,35 @@ CalHeatMap.prototype = {
1147
1225
  }
1148
1226
 
1149
1227
  var parent = this;
1150
- this._domains.push(nextDomainStartTimestamp);
1151
- this._domains.shift();
1228
+ this._domains.set(
1229
+ nextDomainStartTimestamp,
1230
+ this.getSubDomain(nextDomainStartTimestamp).map(function(d) {
1231
+ return {t: parent._domainType[parent.options.subDomain].extractUnit(d), v: null};
1232
+ })
1233
+ );
1234
+ this._domains.remove(this._domains.keys().sort().shift());
1152
1235
 
1153
1236
  this.paint(this.NAVIGATE_RIGHT);
1154
1237
 
1238
+ var domains = this._domains.keys().sort();
1239
+
1155
1240
  this.getDatas(
1156
1241
  this.options.data,
1157
- new Date(this._domains[this._domains.length-1]),
1158
- this.getSubDomain(this._domains[this._domains.length-1]).pop(),
1159
- function(data) {
1160
- parent.fill(data, parent.svg);
1242
+ new Date(parseInt(domains[domains.length-1], 10)),
1243
+ this.getSubDomain(parseInt(domains[domains.length-1], 10)).pop(),
1244
+ function() {
1245
+ parent.fill();
1161
1246
  }
1162
1247
  );
1163
1248
 
1164
- this.afterLoadNextDomain(new Date(this._domains[this._domains.length-1]));
1249
+ this.afterLoadNextDomain(new Date(parseInt(domains[domains.length-1], 10)));
1165
1250
 
1166
1251
  if (this.maxDomainIsReached(this.getNextDomain().getTime())) {
1167
1252
  this.onMaxDomainReached(true);
1168
1253
  }
1169
1254
 
1170
1255
  // Try to "disengage" the min domain reached setting
1171
- if (this._minDomainReached && !this.minDomainIsReached(this._domains[0])) {
1256
+ if (this._minDomainReached && !this.minDomainIsReached(domains[0])) {
1172
1257
  this.onMinDomainReached(false);
1173
1258
  }
1174
1259
 
@@ -1190,28 +1275,35 @@ CalHeatMap.prototype = {
1190
1275
  var previousDomainStartTimestamp = this.getPreviousDomain().getTime();
1191
1276
 
1192
1277
  var parent = this;
1193
- this._domains.unshift(previousDomainStartTimestamp);
1194
- this._domains.pop();
1278
+ this._domains.set(
1279
+ previousDomainStartTimestamp,
1280
+ this.getSubDomain(previousDomainStartTimestamp).map(function(d) {
1281
+ return {t: parent._domainType[parent.options.subDomain].extractUnit(d), v: null};
1282
+ })
1283
+ );
1284
+ this._domains.remove(this._domains.keys().sort().pop());
1195
1285
 
1196
1286
  this.paint(this.NAVIGATE_LEFT);
1197
1287
 
1288
+ var domains = this._domains.keys().sort();
1289
+
1198
1290
  this.getDatas(
1199
1291
  this.options.data,
1200
- new Date(this._domains[0]),
1201
- this.getSubDomain(this._domains[0]).pop(),
1202
- function(data) {
1203
- parent.fill(data, parent.svg);
1292
+ new Date(parseInt(domains[0], 10)),
1293
+ this.getSubDomain(parseInt(domains[0], 10)).pop(),
1294
+ function() {
1295
+ parent.fill();
1204
1296
  }
1205
1297
  );
1206
1298
 
1207
- this.afterLoadPreviousDomain(new Date(this._domains[0]));
1299
+ this.afterLoadPreviousDomain(new Date(parseInt(domains[0], 10)));
1208
1300
 
1209
1301
  if (this.minDomainIsReached(previousDomainStartTimestamp)) {
1210
1302
  this.onMinDomainReached(true);
1211
1303
  }
1212
1304
 
1213
1305
  // Try to "disengage" the max domain reached setting
1214
- if (this._maxDomainReached && !this.maxDomainIsReached(this._domains[this._domains.length-1])) {
1306
+ if (this._maxDomainReached && !this.maxDomainIsReached(domains[domains.length-1])) {
1215
1307
  this.onMaxDomainReached(false);
1216
1308
  }
1217
1309
 
@@ -1236,6 +1328,7 @@ CalHeatMap.prototype = {
1236
1328
  return (this.options.minDate !== null && (this.options.minDate.getTime() >= datetimestamp));
1237
1329
  },
1238
1330
 
1331
+
1239
1332
  // =========================================================================//
1240
1333
  // PAINTING : LEGEND //
1241
1334
  // =========================================================================//
@@ -1245,9 +1338,10 @@ CalHeatMap.prototype = {
1245
1338
  var parent = this;
1246
1339
  var legend = this.root;
1247
1340
 
1248
- switch(this.options.legendVerticalPosition) {
1249
- case "top" : legend = legend.insert("svg", ".graph"); break;
1250
- default : legend = legend.append("svg");
1341
+ if (this.options.legendVerticalPosition === "top") {
1342
+ legend = legend.insert("svg", ".graph");
1343
+ } else {
1344
+ legend = legend.append("svg");
1251
1345
  }
1252
1346
 
1253
1347
  var legendWidth =
@@ -1260,7 +1354,7 @@ CalHeatMap.prototype = {
1260
1354
  .attr("height", this.options.legendCellSize + this.options.legendMargin[0] + this.options.legendMargin[2])
1261
1355
  .attr("width", width)
1262
1356
  .append("g")
1263
- .attr("transform", function(d) {
1357
+ .attr("transform", function() {
1264
1358
  switch(parent.options.legendHorizontalPosition) {
1265
1359
  case "right" : return "translate(" + (width - legendWidth) + ")";
1266
1360
  case "middle" :
@@ -1289,7 +1383,6 @@ CalHeatMap.prototype = {
1289
1383
  legendItem
1290
1384
  .append("title")
1291
1385
  .text(function(d) {
1292
- var nextThreshold = parent.options.legend[d+1];
1293
1386
  if (d === 0) {
1294
1387
  return (parent.options.legendTitleFormat.lower).format({
1295
1388
  min: parent.options.legend[d],
@@ -1309,105 +1402,30 @@ CalHeatMap.prototype = {
1309
1402
 
1310
1403
  },
1311
1404
 
1312
- // =========================================================================//
1313
- // PAINTING : SUBDOMAIN FILLING //
1314
- // =========================================================================//
1315
-
1316
- /**
1317
- * Colorize all rectangles according to their items count
1318
- *
1319
- * @param {[type]} data [description]
1320
- */
1321
- display: function(data, domain) {
1322
- var parent = this;
1323
-
1324
- domain.each(function(domainUnit) {
1325
-
1326
- if (data.hasOwnProperty(domainUnit) || parent.options.considerMissingDataAsZero) {
1327
- d3.select(this).selectAll(".graph-subdomain-group rect")
1328
- .attr("class", function(d) {
1329
- var subDomainUnit = parent._domainType[parent.options.subDomain].extractUnit(d);
1330
-
1331
- var htmlClass = "graph-rect" + parent.getHighlightClassName(d);
1332
-
1333
- var value;
1334
-
1335
- if (data.hasOwnProperty(domainUnit) && data[domainUnit].hasOwnProperty(subDomainUnit)) {
1336
- htmlClass += " " + parent.legend(data[domainUnit][subDomainUnit]);
1337
- } else if (parent.options.considerMissingDataAsZero) {
1338
- htmlClass += " " + parent.legend(0);
1339
- }
1340
-
1341
- if (parent.options.onClick !== null) {
1342
- htmlClass += " hover_cursor";
1343
- }
1344
-
1345
- return htmlClass;
1346
- })
1347
- .on("click", function(d) {
1348
- if (parent.options.onClick !== null) {
1349
- var subDomainUnit = parent._domainType[parent.options.subDomain].extractUnit(d);
1350
- return parent.onClick(
1351
- d,
1352
- (data[domainUnit].hasOwnProperty(subDomainUnit) || parent.options.considerMissingDataAsZero ? data[domainUnit][subDomainUnit] : null)
1353
- );
1354
- }
1355
- });
1356
-
1357
- d3.select(this).selectAll(".graph-subdomain-group title")
1358
- .text(function(d) {
1359
- var subDomainUnit = parent._domainType[parent.options.subDomain].extractUnit(d);
1360
-
1361
- if ((data.hasOwnProperty(domainUnit) && data[domainUnit].hasOwnProperty(subDomainUnit) && data[domainUnit][subDomainUnit] !== null) || parent.options.considerMissingDataAsZero){
1362
-
1363
- if (data.hasOwnProperty(domainUnit) && data[domainUnit].hasOwnProperty(subDomainUnit)) {
1364
- value = data[domainUnit][subDomainUnit];
1365
- } else if (parent.options.considerMissingDataAsZero) {
1366
- value = 0;
1367
- }
1368
-
1369
- return (parent.options.subDomainTitleFormat.filled).format({
1370
- count: parent.formatNumber(value),
1371
- name: parent.options.itemName[(value !== 1 ? 1 : 0)],
1372
- connector: parent._domainType[parent.options.subDomain].format.connector,
1373
- date: parent.formatDate(d, parent.options.subDomainDateFormat)
1374
- });
1375
- } else {
1376
- return (parent.options.subDomainTitleFormat.empty).format({
1377
- date: parent.formatDate(d, parent.options.subDomainDateFormat)
1378
- });
1379
- };
1380
- });
1381
-
1382
-
1383
- }
1384
- }
1385
- );
1386
- return true;
1387
- },
1388
-
1389
1405
  // =========================================================================//
1390
1406
  // POSITIONNING //
1391
1407
  // =========================================================================//
1392
1408
 
1393
1409
  positionSubDomainX: function(d) {
1394
- var index = this._domainType[this.options.subDomain].position.x(d);
1410
+ var index = this._domainType[this.options.subDomain].position.x(new Date(d));
1395
1411
  return index * this.options.cellSize + index * this.options.cellPadding;
1396
1412
  },
1397
1413
 
1398
1414
  positionSubDomainY: function(d) {
1399
- var index = this._domainType[this.options.subDomain].position.y(d);
1415
+ var index = this._domainType[this.options.subDomain].position.y(new Date(d));
1400
1416
  return index * this.options.cellSize + index * this.options.cellPadding;
1401
1417
  },
1402
1418
 
1403
1419
  /**
1404
1420
  * Return a classname if the specified date should be highlighted
1405
1421
  *
1406
- * @param Date d a date
1422
+ * @param timestamp date Date of the current subDomain
1407
1423
  * @return String the highlight class
1408
1424
  */
1409
1425
  getHighlightClassName: function(d)
1410
1426
  {
1427
+ d = new Date(d);
1428
+
1411
1429
  if (this.options.highlight.length > 0) {
1412
1430
  for (var i in this.options.highlight) {
1413
1431
  if (this.options.highlight[i] instanceof Date && this.dateIsEqual(this.options.highlight[i], d)) {
@@ -1468,6 +1486,7 @@ CalHeatMap.prototype = {
1468
1486
  }
1469
1487
  },
1470
1488
 
1489
+
1471
1490
  // =========================================================================//
1472
1491
  // DOMAIN COMPUTATION //
1473
1492
  // =========================================================================//
@@ -1683,6 +1702,7 @@ CalHeatMap.prototype = {
1683
1702
  case "hour" : return this.getHourDomain(date, computeHourSubDomainSize(date, this.options.domain));
1684
1703
  case "x_day" :
1685
1704
  case "day" : return this.getDayDomain(date, computeDaySubDomainSize(date, this.options.domain));
1705
+ case "x_week":
1686
1706
  case "week" : return this.getWeekDomain(date, computeWeekSubDomainSize(date, this.options.domain));
1687
1707
  case "x_month":
1688
1708
  case "month" : return this.getMonthDomain(date, 12);
@@ -1690,11 +1710,11 @@ CalHeatMap.prototype = {
1690
1710
  },
1691
1711
 
1692
1712
  getNextDomain: function() {
1693
- return this.getDomain(this._domains[this._domains.length-1], 2).pop();
1713
+ return this.getDomain(parseInt(this._domains.keys().sort().pop(), 10), 2).pop();
1694
1714
  },
1695
1715
 
1696
1716
  getPreviousDomain: function() {
1697
- return this.getDomain(this._domains[0], -1)[0];
1717
+ return this.getDomain(parseInt(this._domains.keys().sort().shift(), 10), -1)[0];
1698
1718
  },
1699
1719
 
1700
1720
  /**
@@ -1731,46 +1751,60 @@ CalHeatMap.prototype = {
1731
1751
  // =========================================================================//
1732
1752
 
1733
1753
  /**
1734
- * @todo Add check for empty data
1754
+ * Fetch and interpret data from the datasource
1735
1755
  *
1736
- * @return bool True if the calendar was filled with the passed data
1737
- */
1738
- fill: function(datas, domain) {
1739
- var response = this.display(this.parseDatas(datas), domain);
1740
- this.onComplete();
1741
- return response;
1742
- },
1743
-
1744
- /**
1745
- * Interpret the data property
1756
+ * @param string|object source
1757
+ * @param Date startDate
1758
+ * @param Date endDate
1759
+ * @param function callback
1760
+ * @param function|boolean afterLoad function used to convert the data into a json object. Use true to use the afterLoad callback
1761
+ * @param updateMode
1746
1762
  *
1747
1763
  * @return mixed
1748
- * - True if no data to load
1749
- * - False if data is loaded asynchornously
1750
- * - json object
1764
+ * - True if there are no data to load
1765
+ * - False if data are loaded asynchronously
1751
1766
  */
1752
- getDatas: function(source, startDate, endDate, callback) {
1753
- var parent = this;
1767
+ getDatas: function(source, startDate, endDate, callback, afterLoad, updateMode) {
1768
+ var self = this;
1769
+ if (arguments.length < 5) {
1770
+ afterLoad = true;
1771
+ }
1772
+ if (arguments.length < 6) {
1773
+ updateMode = this.APPEND_ON_UPDATE;
1774
+ }
1775
+ var _callback = function(data) {
1776
+ if (afterLoad !== false) {
1777
+ if (typeof afterLoad === "function") {
1778
+ data = afterLoad(data);
1779
+ } else if (typeof (self.options.afterLoadData) === "function") {
1780
+ data = self.options.afterLoadData(data);
1781
+ } else {
1782
+ console.log("Provided callback for afterLoadData is not a function.");
1783
+ return {};
1784
+ }
1785
+ }
1786
+ self.parseDatas(data, updateMode);
1787
+ callback();
1788
+ };
1754
1789
 
1755
1790
  switch(typeof source) {
1756
1791
  case "string" :
1757
1792
  if (source === "") {
1758
- this.onComplete();
1793
+ _callback({});
1759
1794
  return true;
1760
1795
  } else {
1761
-
1762
1796
  switch(this.options.dataType) {
1763
1797
  case "json" :
1764
- d3.json(this.parseURI(source, startDate, endDate), callback);
1798
+ d3.json(this.parseURI(source, startDate, endDate), _callback);
1765
1799
  break;
1766
1800
  case "csv" :
1767
- d3.csv(this.parseURI(source, startDate, endDate), callback);
1801
+ d3.csv(this.parseURI(source, startDate, endDate), _callback);
1768
1802
  break;
1769
1803
  case "tsv" :
1770
- d3.tsv(this.parseURI(source, startDate, endDate), callback);
1804
+ d3.tsv(this.parseURI(source, startDate, endDate), _callback);
1771
1805
  break;
1772
1806
  case "text" :
1773
- d3.text(this.parseURI(source, startDate, endDate), "text/plain", callback);
1807
+ d3.text(this.parseURI(source, startDate, endDate), "text/plain", _callback);
1774
1808
  break;
1775
1809
  }
1776
1810
 
@@ -1779,50 +1813,54 @@ CalHeatMap.prototype = {
1779
1813
  break;
1780
1814
  case "object" :
1781
1815
  // @todo Check that it's a valid JSON object
1782
- callback(source);
1816
+ _callback(source);
1783
1817
  }
1784
1818
 
1785
1819
  return true;
1786
1820
  },
1787
1821
 
1788
1822
  /**
1789
- * Convert a JSON result into the expected format
1823
+ * Populate the calendar internal data
1824
+ *
1825
+ * @param object data
1826
+ * @param constant updateMode
1790
1827
  *
1791
- * @param {[type]} data [description]
1792
- * @return {[type]} [description]
1828
+ * @return void
1793
1829
  */
1794
- parseDatas: function(data) {
1795
- var stats = {};
1830
+ parseDatas: function(data, updateMode) {
1796
1831
 
1797
- if (typeof (this.options.afterLoadData) === "function") {
1798
- data = this.options.afterLoadData(data);
1799
- } else {
1800
- console.log("Provided callback for afterLoadData is not a function.");
1801
- return {};
1832
+ if (updateMode === this.RESET_ALL_ON_UPDATE) {
1833
+ this._domains.forEach(function(key, value) {
1834
+ value.forEach(function(element, index, array) {
1835
+ array[index].v = null;
1836
+ });
1837
+ });
1802
1838
  }
1803
1839
 
1840
+ var domainKeys = this._domains.keys();
1841
+ var subDomainStep = this._domains.get(domainKeys[0])[1].t - this._domains.get(domainKeys[0])[0].t;
1842
+
1804
1843
  for (var d in data) {
1805
1844
  var date = new Date(d*1000);
1806
1845
  var domainUnit = this.getDomain(date)[0].getTime();
1807
1846
 
1808
- // Don't record datas not relevant to the current domain
1809
- if (this._domains.indexOf(domainUnit) < 0) {
1810
- continue;
1811
- }
1847
+ // Record only datas relevant to the current domain
1848
+ if (this._domains.has(domainUnit)) {
1849
+ var subDomainUnit = this._domainType[this.options.subDomain].extractUnit(date);
1850
+ var subDomainsData = this._domains.get(domainUnit);
1851
+ var index = Math.floor((subDomainUnit - domainUnit) / subDomainStep);
1812
1852
 
1813
- var subDomainUnit = this._domainType[this.options.subDomain].extractUnit(date);
1814
- if (typeof stats[domainUnit] === "undefined") {
1815
- stats[domainUnit] = {};
1816
- }
1817
-
1818
- if (typeof stats[domainUnit][subDomainUnit] !== "undefined") {
1819
- stats[domainUnit][subDomainUnit] += data[d];
1820
- } else {
1821
- stats[domainUnit][subDomainUnit] = data[d];
1853
+ if (updateMode === this.RESET_SINGLE_ON_UPDATE) {
1854
+ subDomainsData[index].v = data[d];
1855
+ } else {
1856
+ if (!isNaN(subDomainsData[index].v)) {
1857
+ subDomainsData[index].v += data[d];
1858
+ } else {
1859
+ subDomainsData[index].v = data[d];
1860
+ }
1861
+ }
1822
1862
  }
1823
1863
  }
1824
-
1825
- return stats;
1826
1864
  },
1827
1865
 
1828
1866
  parseURI: function(str, startDate, endDate) {
@@ -1849,6 +1887,35 @@ CalHeatMap.prototype = {
1849
1887
  return this.loadPreviousDomain();
1850
1888
  },
1851
1889
 
1890
+ /**
1891
+ * Update the calendar with new data
1892
+ *
1893
+ * @param object|string dataSource The calendar's datasource, same type as this.options.data
1894
+ * @param boolean|function afterLoad Whether to execute afterLoad() on the data. Pass directly a function
1895
+ * if you don't want to use the afterLoad() callback
1896
+ */
1897
+ update: function(dataSource, afterLoad, updateMode) {
1898
+ if (arguments.length < 2) {
1899
+ afterLoad = true;
1900
+ }
1901
+ if (arguments.length < 3) {
1902
+ updateMode = this.RESET_ALL_ON_UPDATE;
1903
+ }
1904
+
1905
+ var domains = this._domains.keys().sort();
1906
+ var self = this;
1907
+ this.getDatas(
1908
+ dataSource,
1909
+ new Date(parseInt(domains[0], 10)),
1910
+ this.getSubDomain(parseInt(domains[domains.length-1], 10)).pop(),
1911
+ function() {
1912
+ self.fill();
1913
+ },
1914
+ afterLoad,
1915
+ updateMode
1916
+ );
1917
+ },
1918
+
1852
1919
  getSVG: function() {
1853
1920
  var styles = {
1854
1921
  ".graph": {},
@@ -1949,37 +2016,43 @@ CalHeatMap.prototype = {
1949
2016
  };
1950
2017
 
1951
2018
  var DomainPosition = function() {
1952
- this.positions = [];
2019
+ this.positions = d3.map();
1953
2020
  };
1954
2021
 
1955
- DomainPosition.prototype.getPosition = function(i) {
1956
- return this.positions[i];
2022
+ DomainPosition.prototype.getPosition = function(d) {
2023
+ return this.positions.get(d);
1957
2024
  };
1958
2025
 
1959
- DomainPosition.prototype.getLast = function() {
1960
- return this.positions[this.positions.length-1];
2026
+ DomainPosition.prototype.getPositionFromIndex = function(i) {
2027
+ var domains = this.positions.keys().sort();
2028
+ return this.positions.get(domains[i]);
1961
2029
  };
1962
2030
 
1963
- DomainPosition.prototype.pushPosition = function(dim) {
1964
- this.positions.push(dim);
2031
+ DomainPosition.prototype.getLast = function() {
2032
+ var domains = this.positions.keys().sort();
2033
+ return this.positions.get(domains[domains.length-1]);
1965
2034
  };
1966
2035
 
1967
- DomainPosition.prototype.unshiftPosition = function(dim) {
1968
- this.positions.unshift(dim);
2036
+ DomainPosition.prototype.setPosition = function(d, dim) {
2037
+ this.positions.set(d, dim);
1969
2038
  };
1970
2039
 
1971
- DomainPosition.prototype.shiftRight = function(exitingDomainDim) {
1972
- for(var i in this.positions) {
1973
- this.positions[i] -= exitingDomainDim;
1974
- }
1975
- this.positions.shift();
2040
+ DomainPosition.prototype.shiftRightBy = function(exitingDomainDim) {
2041
+ this.positions.forEach(function(key, value) {
2042
+ this.set(key, value - exitingDomainDim);
2043
+ });
2044
+
2045
+ var domains = this.positions.keys().sort();
2046
+ this.positions.remove(domains[0]);
1976
2047
  };
1977
2048
 
1978
- DomainPosition.prototype.shiftLeft = function(enteringDomainDim) {
1979
- for(var i in this.positions) {
1980
- this.positions[i] += enteringDomainDim;
1981
- }
1982
- this.positions.pop();
2049
+ DomainPosition.prototype.shiftLeftBy = function(enteringDomainDim) {
2050
+ this.positions.forEach(function(key, value) {
2051
+ this.set(key, value + enteringDomainDim);
2052
+ });
2053
+
2054
+ var domains = this.positions.keys().sort();
2055
+ this.positions.remove(domains[domains.length-1]);
1983
2056
  };
1984
2057
 
1985
2058
 
@@ -2023,7 +2096,7 @@ function mergeRecursive(obj1, obj2) {
2023
2096
  * AMD Loader
2024
2097
  */
2025
2098
  if (typeof define === "function" && define.amd) {
2026
- define(["d3"], function(d3) {
2099
+ define(["d3"], function() {
2027
2100
  return CalHeatMap;
2028
2101
  });
2029
2102
  }