cal_heatmap_rails 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  }