cal_heatmap_rails 0.0.1

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.
@@ -0,0 +1,2029 @@
1
+ /*! cal-heatmap v3.0.9 (Thu Aug 01 2013 18:58:29)
2
+ * ---------------------------------------------
3
+ * Cal-Heatmap is a javascript module to create calendar heatmap to visualize time series data, a la github contribution graph
4
+ * https://github.com/kamisama/cal-heatmap
5
+ * Licensed under the MIT license
6
+ * Copyright 2013 Wan Qi Chen
7
+ */
8
+
9
+ var CalHeatMap = function() {
10
+
11
+ "use strict";
12
+
13
+ var self = this;
14
+
15
+ var allowedDataType = ["json", "csv", "tsv", "txt"];
16
+
17
+ // Default settings
18
+ this.options = {
19
+ // selector string of the container to append the graph to
20
+ // Accept any string value accepted by document.querySelector or CSS3
21
+ // or an Element object
22
+ itemSelector : "#cal-heatmap",
23
+
24
+ // Whether to paint the calendar on init()
25
+ // Used by testsuite to reduce testing time
26
+ paintOnLoad : true,
27
+
28
+ // ================================================
29
+ // DOMAIN
30
+ // ================================================
31
+
32
+ // Number of domain to display on the graph
33
+ range : 12,
34
+
35
+ // Size of each cell, in pixel
36
+ cellSize : 10,
37
+
38
+ // Padding between each cell, in pixel
39
+ cellPadding : 2,
40
+
41
+ // For rounded subdomain rectangles, in pixels
42
+ cellRadius: 0,
43
+
44
+ domainGutter : 2,
45
+
46
+ domainMargin: [0,0,0,0],
47
+
48
+ domain : "hour",
49
+
50
+ subDomain : "min",
51
+
52
+ // First day of the week is Monday
53
+ // 0 to start the week on Sunday
54
+ weekStartOnMonday : true,
55
+
56
+ // Start date of the graph
57
+ // @default now
58
+ start : new Date(),
59
+
60
+ minDate : null,
61
+
62
+ maxDate: null,
63
+
64
+ // URL, where to fetch the original datas
65
+ data : "",
66
+
67
+ dataType: allowedDataType[0],
68
+
69
+ // Whether to consider missing date:value from the datasource
70
+ // as equal to 0, or just leave them as missing
71
+ considerMissingDataAsZero: false,
72
+
73
+ // Load remote data on calendar creation
74
+ // When false, the calendar will be left empty
75
+ loadOnInit : true,
76
+
77
+ // Calendar orientation
78
+ // false : display domains side by side
79
+ // true : display domains one under the other
80
+ verticalOrientation: false,
81
+
82
+ // Domain dynamic width/height
83
+ // The width on a domain depends on the number of
84
+ domainDynamicDimension: true,
85
+
86
+ // Domain Label properties
87
+ label: {
88
+ // valid : top, right, bottom, left
89
+ position: "bottom",
90
+
91
+ // Valid : left, center, right
92
+ // Also valid are the direct svg values : start, middle, end
93
+ align: "center",
94
+
95
+ // By default, there is no margin/padding around the label
96
+ offset: {
97
+ x: 0,
98
+ y: 0
99
+ },
100
+
101
+ rotate: null,
102
+
103
+ width: 100
104
+ },
105
+
106
+ // ================================================
107
+ // LEGEND
108
+ // ================================================
109
+
110
+ // Threshold for the legend
111
+ legend : [10,20,30,40],
112
+
113
+ // Whether to display the legend
114
+ displayLegend : true,
115
+
116
+ legendCellSize: 10,
117
+
118
+ legendCellPadding: 2,
119
+
120
+ legendMargin: [10, 0, 0, 0],
121
+
122
+ // Legend vertical position
123
+ // top : place legend above calendar
124
+ // bottom: place legend below the calendar
125
+ legendVerticalPosition: "bottom",
126
+
127
+ // Legend horizontal position
128
+ // accepted values : left, center, right
129
+ legendHorizontalPosition: "left",
130
+
131
+
132
+ // ================================================
133
+ // HIGHLIGHT
134
+ // ================================================
135
+
136
+ // List of dates to highlight
137
+ // Valid values :
138
+ // - [] : don't highlight anything
139
+ // - "now" : highlight the current date
140
+ // - an array of Date objects : highlight the specified dates
141
+ highlight : [],
142
+
143
+ // ================================================
144
+ // TEXT FORMATTING / i18n
145
+ // ================================================
146
+
147
+ // Name of the items to represent in the calendar
148
+ itemName : ["item", "items"],
149
+
150
+ // Formatting of the domain label
151
+ // @default: null, will use the formatting according to domain type
152
+ // Accept a string used as specifier by d3.time.format()
153
+ // or a function
154
+ //
155
+ // Refer to https://github.com/mbostock/d3/wiki/Time-Formatting
156
+ // for accepted date formatting used by d3.time.format()
157
+ domainLabelFormat: null,
158
+
159
+ // Formatting of the title displayed when hovering a subDomain cell
160
+ subDomainTitleFormat : {
161
+ empty: "{date}",
162
+ filled: "{count} {name} {connector} {date}"
163
+ },
164
+
165
+ // Formatting of the {date} used in subDomainTitleFormat
166
+ // @default : null, will use the formatting according to subDomain type
167
+ // Accept a string used as specifier by d3.time.format()
168
+ // or a function
169
+ //
170
+ // Refer to https://github.com/mbostock/d3/wiki/Time-Formatting
171
+ // for accepted date formatting used by d3.time.format()
172
+ subDomainDateFormat: null,
173
+
174
+ // Formatting of the text inside each subDomain cell
175
+ // @default: null, no text
176
+ // Accept a string used as specifier by d3.time.format()
177
+ // or a function
178
+ //
179
+ // Refer to https://github.com/mbostock/d3/wiki/Time-Formatting
180
+ // for accepted date formatting used by d3.time.format()
181
+ subDomainTextFormat: null,
182
+
183
+ // Formatting of the title displayed when hovering a legend cell
184
+ legendTitleFormat : {
185
+ lower: "less than {min} {name}",
186
+ inner: "between {down} and {up} {name}",
187
+ upper: "more than {max} {name}"
188
+ },
189
+
190
+ // Animation duration, in ms
191
+ animationDuration : 500,
192
+
193
+ nextSelector: false,
194
+
195
+ previousSelector: false,
196
+
197
+ itemNamespace: "cal-heatmap",
198
+
199
+
200
+ // ================================================
201
+ // CALLBACK
202
+ // ================================================
203
+
204
+ // Callback when clicking on a time block
205
+ onClick : null,
206
+
207
+ // Callback after painting the empty calendar
208
+ afterLoad : null,
209
+
210
+ // Callback after loading the next domain in the calendar
211
+ afterLoadNextDomain : function(start) {},
212
+
213
+ // Callback after loading the previous domain in the calendar
214
+ afterLoadPreviousDomain : function(start) {},
215
+
216
+ // Callback after finishing all actions on the calendar
217
+ onComplete : null,
218
+
219
+ // Callback after fetching the datas, but before applying them to the calendar
220
+ // Used mainly to convert the datas if they're not formatted like expected
221
+ // Takes the fetched "data" object as argument, must return a json object
222
+ // formatted like {timestamp:count, timestamp2:count2},
223
+ afterLoadData : function(data) { return data; },
224
+
225
+ // Callback triggered after calling next().
226
+ // The `status` argument is equal to true if there is no
227
+ // more next domain to load
228
+ //
229
+ // This callback is also executed once, after calling previous(),
230
+ // only when the max domain is reached
231
+ onMaxDomainReached: function(reached) {},
232
+
233
+ // Callback triggered after calling previous().
234
+ // The `status` argument is equal to true if there is no
235
+ // more previous domain to load
236
+ //
237
+ // This callback is also executed once, after calling next(),
238
+ // only when the min domain is reached
239
+ onMinDomainReached: function(reached) {}
240
+ };
241
+
242
+
243
+
244
+ this._domainType = {
245
+ "min" : {
246
+ name: "minute",
247
+ level: 10,
248
+ row: function(d) {return 10;},
249
+ column: function(d) { return 6; },
250
+ position: {
251
+ x : function(d) { return Math.floor(d.getMinutes() / self._domainType.min.row(d)); },
252
+ y : function(d) { return d.getMinutes() % self._domainType.min.row(d);}
253
+ },
254
+ format: {
255
+ date: "%H:%M, %A %B %-e, %Y",
256
+ legend: "",
257
+ connector: "at"
258
+ },
259
+ extractUnit : function(d) { return d.getMinutes(); }
260
+ },
261
+ "hour" : {
262
+ name: "hour",
263
+ level: 20,
264
+ row: function(d) {return 6;},
265
+ column: function(d) {
266
+ switch(self.options.domain) {
267
+ case "day" : return 4;
268
+ case "week" : return 28;
269
+ case "month" : return (self.options.domainDynamicDimension ? self.getEndOfMonth(d).getDate() : 31) * 4;
270
+ }
271
+ },
272
+ position: {
273
+ x : function(d) {
274
+ if (self.options.domain === "month") {
275
+ return Math.floor(d.getHours() / self._domainType.hour.row(d)) + (d.getDate()-1)*4;
276
+ } else if (self.options.domain === "week") {
277
+ return Math.floor(d.getHours() / self._domainType.hour.row(d)) + self.getWeekDay(d)*4;
278
+ }
279
+ return Math.floor(d.getHours() / self._domainType.hour.row(d));
280
+ },
281
+ y : function(d) { return d.getHours() % self._domainType.hour.row(d);}
282
+ },
283
+ format: {
284
+ date: "%Hh, %A %B %-e, %Y",
285
+ legend: "%H:00",
286
+ connector: "at"
287
+ },
288
+ extractUnit : function(d) {
289
+ var formatHour = d3.time.format("%H");
290
+ return d.getFullYear() + "" + self.getDayOfYear(d) + "" + formatHour(d);
291
+ }
292
+ },
293
+ "day" : {
294
+ name: "day",
295
+ level: 30,
296
+ row: function(d) {return 7;},
297
+ column: function(d) {
298
+ d = new Date(d);
299
+ switch(self.options.domain) {
300
+ case "year" : return (self.options.domainDynamicDimension ? (self.getWeekNumber(new Date(d.getFullYear(), 11, 31)) - self.getWeekNumber(new Date(d.getFullYear(), 0)) + 1) : 54);
301
+ case "month" :
302
+ if (self.options.verticalOrientation) {
303
+ return 6;
304
+ }
305
+ return self.options.domainDynamicDimension ? (self.getWeekNumber(new Date(d.getFullYear(), d.getMonth()+1, 0)) - self.getWeekNumber(d) + 1) : 6;
306
+ case "week" : return 1;
307
+ }
308
+ },
309
+ position: {
310
+ x : function(d) {
311
+ switch(self.options.domain) {
312
+ case "week" : return 0;
313
+ case "month" :
314
+ return self.getWeekNumber(d) - self.getWeekNumber(new Date(d.getFullYear(), d.getMonth()));
315
+ case "year" : return self.getWeekNumber(d) ;
316
+ }
317
+ },
318
+ y : function(d) { return self.getWeekDay(d);}
319
+ },
320
+ format: {
321
+ date: "%A %B %-e, %Y",
322
+ legend: "%e %b",
323
+ connector: "on"
324
+ },
325
+ extractUnit : function(d) { return d.getFullYear() + "" + self.getDayOfYear(d); }
326
+ },
327
+ "week" : {
328
+ name: "week",
329
+ level: 40,
330
+ row: function(d) {return 1;},
331
+ column: function(d) {
332
+ d = new Date(d);
333
+ switch(self.options.domain) {
334
+ case "year" : return 54;
335
+ case "month" : return self.getWeekNumber(new Date(d.getFullYear(), d.getMonth()+1, 0)) - self.getWeekNumber(d);
336
+ default: return 1;
337
+ }
338
+ },
339
+ position: {
340
+ x: function(d) {
341
+ switch(self.options.domain) {
342
+ case "year" : return self.getWeekNumber(d);
343
+ case "month" : return self.getWeekNumber(d) - self.getWeekNumber(new Date(d.getFullYear(), d.getMonth())) - 1;
344
+ }
345
+ },
346
+ y: function(d) {
347
+ return 0;
348
+ }
349
+ },
350
+ format: {
351
+ date: "%B Week #%W",
352
+ legend: "%B Week #%W",
353
+ connector: "on"
354
+ },
355
+ extractUnit : function(d) { return self.getWeekNumber(d); }
356
+ },
357
+ "month" : {
358
+ name: "month",
359
+ level: 50,
360
+ row: function(d) {return 1;},
361
+ column: function(d) {return 12;},
362
+ position: {
363
+ x : function(d) { return Math.floor(d.getMonth() / self._domainType.month.row(d)); },
364
+ y : function(d) { return d.getMonth() % self._domainType.month.row(d);}
365
+ },
366
+ format: {
367
+ date: "%B %Y",
368
+ legend: "%B",
369
+ connector: "on"
370
+ },
371
+ extractUnit : function(d) { return d.getMonth(); }
372
+ },
373
+ "year" : {
374
+ name: "year",
375
+ level: 60,
376
+ row: function(d) {return 1;},
377
+ column: function(d) {return 12;},
378
+ position: {
379
+ x : function(d) { return Math.floor(d.getFullYear() / this._domainType.year.row(d)); },
380
+ y : function(d) { return d.getFullYear() % this._domainType.year.row(d);}
381
+ },
382
+ format: {
383
+ date: "%Y",
384
+ legend: "%Y",
385
+ connector: "on"
386
+ },
387
+ extractUnit : function(d) { return d.getFullYear(); }
388
+ }
389
+ };
390
+
391
+ for (var type in this._domainType) {
392
+ this._domainType["x_" + type] = {};
393
+ this._domainType["x_" + type].name = "x_" + type;
394
+ this._domainType["x_" + type].level = this._domainType[type].level;
395
+ this._domainType["x_" + type].row = this._domainType[type].column;
396
+ this._domainType["x_" + type].column = this._domainType[type].row;
397
+ this._domainType["x_" + type].position = {};
398
+ this._domainType["x_" + type].position.x = this._domainType[type].position.y;
399
+ this._domainType["x_" + type].position.y = this._domainType[type].position.x;
400
+ this._domainType["x_" + type].format = this._domainType[type].format;
401
+ this._domainType["x_" + type].extractUnit = this._domainType[type].extractUnit;
402
+ }
403
+
404
+ // Exception : always return the maximum number of weeks
405
+ // to align the label vertically
406
+ this._domainType.x_day.row = function(d) {
407
+ d = new Date(d);
408
+ switch(self.options.domain) {
409
+ case "year" : return (self.options.domainDynamicDimension ? (self.getWeekNumber(new Date(d.getFullYear(), 11, 31)) - self.getWeekNumber(new Date(d.getFullYear(), 0)) + 1) : 54);
410
+ case "month" :
411
+ if (!self.options.verticalOrientation) {
412
+ return 6;
413
+ }
414
+ return self.options.domainDynamicDimension ? (self.getWeekNumber(new Date(d.getFullYear(), d.getMonth()+1, 0)) - self.getWeekNumber(d) + 1) : 6;
415
+ case "week" : return 1;
416
+ }
417
+ };
418
+
419
+
420
+ this.svg = null;
421
+
422
+ this._completed = false;
423
+
424
+ // Record all the valid domains
425
+ // Each domain value is a timestamp in milliseconds
426
+ this._domains = [];
427
+
428
+ var graphDim = {
429
+ width: 0,
430
+ height: 0
431
+ };
432
+
433
+ this.NAVIGATE_LEFT = 1;
434
+ this.NAVIGATE_RIGHT = 2;
435
+
436
+ this.root = null;
437
+
438
+ this._maxDomainReached = false;
439
+ this._minDomainReached = false;
440
+
441
+ this.domainPosition = new DomainPosition();
442
+
443
+ /**
444
+ * Display the graph for the first time
445
+ * @return bool True if the calendar is created
446
+ */
447
+ function _init() {
448
+
449
+ self._domains = self.getDomain(self.options.start).map(function(d) { return d.getTime(); });
450
+
451
+ self.root = d3.select(self.options.itemSelector);
452
+
453
+ self.root.append("svg").attr("class", "graph");
454
+
455
+ if (self.options.paintOnLoad) {
456
+
457
+ self.paint();
458
+
459
+ // =========================================================================//
460
+ // ATTACHING DOMAIN NAVIGATION EVENT //
461
+ // =========================================================================//
462
+ if (self.options.nextSelector !== false) {
463
+ d3.select(self.options.nextSelector).on("click." + self.options.itemNamespace, function(d) {
464
+ d3.event.preventDefault();
465
+ return self.loadNextDomain();
466
+ });
467
+ }
468
+
469
+ if (self.options.previousSelector !== false) {
470
+ d3.select(self.options.previousSelector).on("click." + self.options.itemNamespace, function(d) {
471
+ d3.event.preventDefault();
472
+ return self.loadPreviousDomain();
473
+ });
474
+ }
475
+
476
+ // Display legend if needed
477
+ if (self.options.displayLegend) {
478
+ self.displayLegend(graphDim.width - self.options.domainGutter - self.options.cellPadding);
479
+ }
480
+
481
+ if (self.options.afterLoad !== null) {
482
+ self.afterLoad();
483
+ }
484
+
485
+ // Fill the graph with some datas
486
+ if (self.options.loadOnInit) {
487
+ self.getDatas(
488
+ 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);
493
+ }
494
+ );
495
+ } else {
496
+ self.onComplete();
497
+ }
498
+ }
499
+
500
+ return true;
501
+ }
502
+
503
+
504
+ /**
505
+ *
506
+ *
507
+ * @param int navigationDir
508
+ */
509
+ this.paint = function(navigationDir) {
510
+
511
+ if (typeof navigationDir === "undefined") {
512
+ navigationDir = false;
513
+ }
514
+
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
+ // Painting all the domains
550
+ var domainSvg = self.root.select(".graph")
551
+ .selectAll(".graph-domain")
552
+ .data(self._domains, function(d) { return d;})
553
+ ;
554
+
555
+ var enteringDomainDim = 0;
556
+ var exitingDomainDim = 0;
557
+
558
+
559
+ // =========================================================================//
560
+ // PAINTING DOMAIN //
561
+ // =========================================================================//
562
+
563
+ var svg = domainSvg
564
+ .enter()
565
+ .append("svg")
566
+ .attr("width", function(d, i){
567
+ return w(d, true);
568
+ })
569
+ .attr("height", function(d) {
570
+ return h(d, true);
571
+ })
572
+ .attr("x", function(d, i) {
573
+ if (self.options.verticalOrientation) {
574
+ graphDim.width = w(d, true);
575
+ return 0;
576
+ } else {
577
+ return getDomainPosition(i, graphDim, "width", w(d, true));
578
+ }
579
+ })
580
+ .attr("y", function(d, i) {
581
+ if (self.options.verticalOrientation) {
582
+ return getDomainPosition(i, graphDim, "height", h(d, true));
583
+ } else {
584
+ graphDim.height = h(d, true);
585
+ return 0;
586
+ }
587
+ })
588
+ .attr("class", function(d) {
589
+ var classname = "graph-domain";
590
+ var date = new Date(d);
591
+ switch(self.options.domain) {
592
+ case "hour" : classname += " h_" + date.getHours();
593
+ case "day" : classname += " d_" + date.getDate() + " dy_" + date.getDay();
594
+ case "week" : classname += " w_" + self.getWeekNumber(date);
595
+ case "month" : classname += " m_" + (date.getMonth() + 1);
596
+ case "year" : classname += " y_" + date.getFullYear();
597
+ }
598
+ return classname;
599
+ })
600
+ ;
601
+
602
+ function getDomainPosition(index, graphDim, axis, domainDim) {
603
+ var tmp = 0;
604
+ switch(navigationDir) {
605
+ case false :
606
+ if (index > 0) {
607
+ tmp = graphDim[axis];
608
+ }
609
+
610
+ graphDim[axis] += domainDim;
611
+ self.domainPosition.pushPosition(tmp);
612
+ return tmp;
613
+
614
+ case self.NAVIGATE_RIGHT :
615
+ self.domainPosition.pushPosition(graphDim[axis]);
616
+
617
+ enteringDomainDim = domainDim;
618
+ exitingDomainDim = self.domainPosition.getPosition(1);
619
+
620
+ self.domainPosition.shiftRight(exitingDomainDim);
621
+ return graphDim[axis];
622
+
623
+ case self.NAVIGATE_LEFT :
624
+ tmp = -domainDim;
625
+
626
+ enteringDomainDim = -tmp;
627
+ exitingDomainDim = graphDim[axis] - self.domainPosition.getLast();
628
+
629
+ self.domainPosition.unshiftPosition(tmp);
630
+ self.domainPosition.shiftLeft(enteringDomainDim);
631
+ return tmp;
632
+ }
633
+ }
634
+
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; })
638
+ .attr("class", "domain-background")
639
+ ;
640
+
641
+ // =========================================================================//
642
+ // PAINTING SUBDOMAINS //
643
+ // =========================================================================//
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];
649
+ }
650
+ })
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];
655
+ }
656
+ })
657
+ .attr("class", "graph-subdomain-group")
658
+ ;
659
+
660
+ var rect = subDomainSvgGroup
661
+ .selectAll("svg")
662
+ .data(function(d) { return self.getSubDomain(d); })
663
+ .enter()
664
+ .append("g")
665
+ ;
666
+
667
+ rect
668
+ .append("rect")
669
+ .attr("class", function(d) {
670
+ return "graph-rect" + self.getHighlightClassName(d) + (self.options.onClick !== null ? " hover_cursor" : "");
671
+ })
672
+ .attr("width", self.options.cellSize)
673
+ .attr("height", self.options.cellSize)
674
+ .attr("x", function(d) { return self.positionSubDomainX(d); })
675
+ .attr("y", function(d) { return self.positionSubDomainY(d); })
676
+ .on("click", function(d) {
677
+ if (self.options.onClick !== null) {
678
+ return self.onClick(d, null);
679
+ }
680
+ })
681
+ .call(radius)
682
+ ;
683
+
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
+
693
+
694
+
695
+ // =========================================================================//
696
+ // PAINTING LABEL //
697
+ // =========================================================================//
698
+ svg.append("text")
699
+ .attr("class", "graph-label")
700
+ .attr("y", function(d, i) {
701
+ var y = self.options.domainMargin[0];
702
+ switch(self.options.label.position) {
703
+ case "top" : y += domainVerticalLabelHeight/2; break;
704
+ case "bottom" : y += h(d) + domainVerticalLabelHeight/2;
705
+ }
706
+
707
+ return y + self.options.label.offset.y *
708
+ (
709
+ ((self.options.label.rotate === "right" && self.options.label.position === "right") ||
710
+ (self.options.label.rotate === "left" && self.options.label.position === "left")) ?
711
+ -1 : 1
712
+ );
713
+ })
714
+ .attr("x", function(d, i){
715
+ var x = self.options.domainMargin[3];
716
+ switch(self.options.label.position) {
717
+ case "right" : x += w(d); break;
718
+ case "bottom" :
719
+ case "top" : x += w(d)/2;
720
+ }
721
+
722
+ if (self.options.label.align === "right") {
723
+ return x + domainHorizontalLabelWidth - self.options.label.offset.x *
724
+ (self.options.label.rotate === "right" ? -1 : 1);
725
+ }
726
+ return x + self.options.label.offset.x;
727
+
728
+ })
729
+ .attr("text-anchor", function() {
730
+ switch(self.options.label.align) {
731
+ case "start" :
732
+ case "left" : return "start";
733
+ case "end" :
734
+ case "right" : return "end";
735
+ default : return "middle";
736
+ }
737
+ })
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); })
740
+ .call(domainRotate)
741
+ ;
742
+
743
+ function domainRotate(selection) {
744
+ switch (self.options.label.rotate) {
745
+ case "right" :
746
+ selection
747
+ .attr("transform", function(d) {
748
+ var s = "rotate(90), ";
749
+ switch(self.options.label.position) {
750
+ case "right" : s += "translate(-" + w(d) + " , -" + w(d) + ")"; break;
751
+ case "left" : s += "translate(0, -" + domainHorizontalLabelWidth + ")"; break;
752
+ }
753
+
754
+ return s;
755
+ });
756
+ break;
757
+ case "left" :
758
+ selection
759
+ .attr("transform", function(d) {
760
+ var s = "rotate(270), ";
761
+ switch(self.options.label.position) {
762
+ case "right" : s += "translate(-" + (w(d) + domainHorizontalLabelWidth) + " , " + w(d) + ")"; break;
763
+ case "left" : s += "translate(-" + (domainHorizontalLabelWidth) + " , " + domainHorizontalLabelWidth + ")"; break;
764
+ }
765
+
766
+ return s;
767
+ });
768
+ break;
769
+ }
770
+ }
771
+
772
+
773
+ // Appending a title to each subdomain
774
+ rect.append("title").text(function(d){ return self.formatDate(d, self.options.subDomainDateFormat); });
775
+
776
+
777
+ // =========================================================================//
778
+ // PAINTING DOMAIN SUBDOMAIN CONTENT //
779
+ // =========================================================================//
780
+ if (self.options.subDomainTextFormat !== null) {
781
+ rect
782
+ .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; })
786
+ .attr("text-anchor", "middle")
787
+ .attr("dominant-baseline", "central")
788
+ .text(function(d){ return self.formatDate(d, self.options.subDomainTextFormat); })
789
+ ;
790
+ }
791
+
792
+ // =========================================================================//
793
+ // ANIMATION //
794
+ // =========================================================================//
795
+
796
+ if (navigationDir !== false) {
797
+ 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
+ }
804
+ })
805
+ .attr("y", function(d, i){
806
+ if (self.options.verticalOrientation) {
807
+ return self.domainPosition.getPosition(i);
808
+ } else {
809
+ return 0;
810
+ }
811
+ })
812
+ ;
813
+ }
814
+
815
+ var tempWidth = graphDim.width;
816
+ var tempHeight = graphDim.height;
817
+
818
+ if (self.options.verticalOrientation) {
819
+ graphDim.height += enteringDomainDim - exitingDomainDim;
820
+ } else {
821
+ graphDim.width += enteringDomainDim - exitingDomainDim;
822
+ }
823
+
824
+ // At the time of exit, domainsWidth and domainsHeight already automatically shifted
825
+ domainSvg.exit().transition().duration(self.options.animationDuration)
826
+ .attr("x", function(d, i){
827
+ if (self.options.verticalOrientation) {
828
+ return 0;
829
+ } else {
830
+ switch(navigationDir) {
831
+ case self.NAVIGATE_LEFT : return Math.min(graphDim.width, tempWidth);
832
+ case self.NAVIGATE_RIGHT : return -w(d, true);
833
+ }
834
+ }
835
+ })
836
+ .attr("y", function(d){
837
+ if (self.options.verticalOrientation) {
838
+ switch(navigationDir) {
839
+ case self.NAVIGATE_LEFT : return Math.min(graphDim.height, tempHeight);
840
+ case self.NAVIGATE_RIGHT : return -h(d, true);
841
+ }
842
+ } else {
843
+ return 0;
844
+ }
845
+ })
846
+ .remove()
847
+ ;
848
+
849
+ // Resize the graph
850
+ self.root.select(".graph").transition().duration(self.options.animationDuration)
851
+ .attr("width", function() { return graphDim.width - self.options.domainGutter - self.options.cellPadding; })
852
+ .attr("height", function() { return graphDim.height - self.options.domainGutter - self.options.cellPadding; })
853
+ ;
854
+
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
+ }
861
+ };
862
+
863
+
864
+ this.init = function(settings) {
865
+
866
+ self.options = mergeRecursive(self.options, settings);
867
+
868
+ if (!this._domainType.hasOwnProperty(self.options.domain) || self.options.domain === "min" || self.options.domain.substring(0, 2) === "x_") {
869
+ console.log("The domain '" + self.options.domain + "' is not valid");
870
+ return false;
871
+ }
872
+
873
+ if (!this._domainType.hasOwnProperty(self.options.subDomain) || self.options.subDomain === "year") {
874
+ console.log("The subDomain '" + self.options.subDomain + "' is not valid");
875
+ return false;
876
+ }
877
+
878
+ if (this._domainType[self.options.domain].level <= this._domainType[self.options.subDomain].level) {
879
+ console.log("'" + self.options.subDomain + "' is not a valid subDomain to '" + self.options.domain + "'");
880
+ return false;
881
+ }
882
+
883
+
884
+ // Set the most suitable subdomain for the domain
885
+ // if subDomain is not explicitly specified
886
+ if (!settings.hasOwnProperty("subDomain")) {
887
+ switch(self.options.domain) {
888
+ case "year" : self.options.subDomain = "month"; break;
889
+ case "month" : self.options.subDomain = "day"; break;
890
+ case "week" : self.options.subDomain = "day"; break;
891
+ case "day" : self.options.subDomain = "hour"; break;
892
+ default : self.options.subDomain = "min";
893
+ }
894
+ }
895
+
896
+ if (allowedDataType.indexOf(self.options.dataType) < 0) {
897
+ console.log("The data type '" + self.options.dataType + "' is not valid data type");
898
+ return false;
899
+ }
900
+
901
+ if (self.options.subDomainDateFormat === null) {
902
+ self.options.subDomainDateFormat = this._domainType[self.options.subDomain].format.date;
903
+ }
904
+
905
+ if (self.options.domainLabelFormat === null) {
906
+ self.options.domainLabelFormat = this._domainType[self.options.domain].format.legend;
907
+ }
908
+
909
+ // Auto-align label, depending on it's position
910
+ if (!settings.hasOwnProperty("label") || (settings.hasOwnProperty("label") && !settings.label.hasOwnProperty("align"))) {
911
+ switch(self.options.label.position) {
912
+ case "left" : self.options.label.align = "right"; break;
913
+ case "right" : self.options.label.align = "left"; break;
914
+ default : self.options.label.align = "center";
915
+ }
916
+
917
+
918
+ if (self.options.label.rotate === "left") {
919
+ self.options.label.align = "right";
920
+ } else if (self.options.label.rotate === "right") {
921
+ self.options.label.align = "left";
922
+ }
923
+
924
+ }
925
+
926
+ if (!settings.hasOwnProperty("label") || (settings.hasOwnProperty("label") && !settings.label.hasOwnProperty("offset"))) {
927
+ if (self.options.label.position === "left" || self.options.label.position === "right") {
928
+ self.options.label.offset = {
929
+ x: 10,
930
+ y: 15
931
+ };
932
+ }
933
+ }
934
+
935
+ if (validateSelector(self.options.itemSelector)) {
936
+ console.log("The itemSelector is invalid");
937
+ return false;
938
+ }
939
+
940
+ if (d3.select(self.options.itemSelector)[0][0] === null) {
941
+ console.log("The node specified in itemSelector does not exists");
942
+ return false;
943
+ }
944
+
945
+ if (self.options.nextSelector !== false && validateSelector(self.options.nextSelector)) {
946
+ console.log("The nextSelector is invalid");
947
+ return false;
948
+ }
949
+
950
+ if (self.options.previousSelector !== false && validateSelector(self.options.previousSelector)) {
951
+ console.log("The previousSelector is invalid");
952
+ return false;
953
+ }
954
+
955
+ if (typeof self.options.itemNamespace !== "string" || self.options.itemNamespace === "") {
956
+ console.log("itemNamespace can not be empty, falling back to cal-heatmap");
957
+ self.options.itemNamespace = "cal-heatmap";
958
+ }
959
+
960
+ if (typeof self.options.domainMargin === "number") {
961
+ self.options.domainMargin = [self.options.domainMargin, self.options.domainMargin, self.options.domainMargin, self.options.domainMargin];
962
+ }
963
+
964
+ if (Array.isArray(self.options.domainMargin)) {
965
+ switch(self.options.domainMargin.length) {
966
+ case 0 : self.options.domainMargin = [0, 0, 0, 0]; break;
967
+ case 1 : self.options.domainMargin = [self.options.domainMargin, self.options.domainMargin, self.options.domainMargin, self.options.domainMargin]; break;
968
+ case 2 : self.options.domainMargin = [self.options.domainMargin[0], self.options.domainMargin[1], self.options.domainMargin[0], self.options.domainMargin[1]]; break;
969
+ case 3 : self.options.domainMargin = [self.options.domainMargin[0], self.options.domainMargin[1], self.options.domainMargin[2], self.options.domainMargin[1]]; break;
970
+ case 4 : self.options.domainMargin = self.options.domainMargin; break;
971
+ default : self.options.domainMargin.splice(4);
972
+ }
973
+ }
974
+
975
+ if (typeof self.options.itemName === "string") {
976
+ self.options.itemName = [self.options.itemName, self.options.itemName + "s"];
977
+ } else if (Array.isArray(self.options.itemName) && self.options.itemName.length === 1) {
978
+ self.options.itemName = [self.options.itemName[0], self.options.itemName[0] + "s"];
979
+ }
980
+
981
+ // Don't touch these settings
982
+ var s = ["data", "onComplete", "onClick", "afterLoad", "afterLoadData", "afterLoadPreviousDomain", "afterLoadNextDomain"];
983
+
984
+ for (var k in s) {
985
+ if (settings.hasOwnProperty(s[k])) {
986
+ self.options[s[k]] = settings[s[k]];
987
+ }
988
+ }
989
+
990
+ if (typeof self.options.highlight === "string") {
991
+ if (self.options.highlight === "now") {
992
+ self.options.highlight = [new Date()];
993
+ } else {
994
+ self.options.highlight = [];
995
+ }
996
+ } else if (Array.isArray(self.options.highlight)) {
997
+ var i = self.options.highlight.indexOf("now");
998
+ if (i !== -1) {
999
+ self.options.highlight.splice(i, 1);
1000
+ self.options.highlight.push(new Date());
1001
+ }
1002
+ }
1003
+
1004
+
1005
+ function validateSelector(selector) {
1006
+ return ((!(selector instanceof Element) && typeof selector !== "string") || selector === "");
1007
+ }
1008
+
1009
+ return _init();
1010
+
1011
+ };
1012
+
1013
+ };
1014
+
1015
+ CalHeatMap.prototype = {
1016
+
1017
+
1018
+ // =========================================================================//
1019
+ // CALLBACK //
1020
+ // =========================================================================//
1021
+
1022
+ /**
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
1026
+ */
1027
+ onClick : function(d, itemNb) {
1028
+ if (typeof this.options.onClick === "function") {
1029
+ return this.options.onClick(d, itemNb);
1030
+ } else {
1031
+ console.log("Provided callback for onClick is not a function.");
1032
+ return false;
1033
+ }
1034
+ },
1035
+
1036
+ /**
1037
+ * Callback to fire after drawing the calendar, but before filling it
1038
+ */
1039
+ 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
+ }
1046
+ },
1047
+
1048
+ /**
1049
+ * Callback to fire at the end, when all actions on the calendar are completed
1050
+ */
1051
+ onComplete : function() {
1052
+ if (this.options.onComplete === null || this._completed === true) {
1053
+ return true;
1054
+ }
1055
+
1056
+ 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
+ }
1063
+ },
1064
+
1065
+ /**
1066
+ * Callback after shifting the calendar one domain back
1067
+ * @param Date start Domain start date
1068
+ * @param Date end Domain end date
1069
+ */
1070
+ 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
+ }
1078
+ },
1079
+
1080
+ /**
1081
+ * Callback after shifting the calendar one domain above
1082
+ * @param Date start Domain start date
1083
+ * @param Date end Domain end date
1084
+ */
1085
+ 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
+ }
1093
+ },
1094
+
1095
+ onMinDomainReached: function(reached) {
1096
+ 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
+ }
1103
+ },
1104
+
1105
+ onMaxDomainReached: function(reached) {
1106
+ 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
+ }
1113
+ },
1114
+
1115
+ formatNumber: d3.format(",g"),
1116
+
1117
+ formatDate: function(d, format) {
1118
+ if (typeof format === "undefined") {
1119
+ format = "title";
1120
+ }
1121
+
1122
+ if (typeof format === "function") {
1123
+ return format(d);
1124
+ } else {
1125
+ var f = d3.time.format(format);
1126
+ return f(d);
1127
+ }
1128
+ },
1129
+
1130
+ // =========================================================================//
1131
+ // DOMAIN NAVIGATION //
1132
+ // =========================================================================//
1133
+
1134
+ /**
1135
+ * Shift the calendar one domain forward
1136
+ *
1137
+ * The new domain is loaded only if it's not beyond the maxDate
1138
+ *
1139
+ * @return bool True if the next domain was loaded, else false
1140
+ */
1141
+ loadNextDomain: function() {
1142
+
1143
+ var nextDomainStartTimestamp = this.getNextDomain().getTime();
1144
+
1145
+ if (this._maxDomainReached || this.maxDomainIsReached(nextDomainStartTimestamp)) {
1146
+ return false;
1147
+ }
1148
+
1149
+ var parent = this;
1150
+ this._domains.push(nextDomainStartTimestamp);
1151
+ this._domains.shift();
1152
+
1153
+ this.paint(this.NAVIGATE_RIGHT);
1154
+
1155
+ this.getDatas(
1156
+ 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);
1161
+ }
1162
+ );
1163
+
1164
+ this.afterLoadNextDomain(new Date(this._domains[this._domains.length-1]));
1165
+
1166
+ if (this.maxDomainIsReached(this.getNextDomain().getTime())) {
1167
+ this.onMaxDomainReached(true);
1168
+ }
1169
+
1170
+ // Try to "disengage" the min domain reached setting
1171
+ if (this._minDomainReached && !this.minDomainIsReached(this._domains[0])) {
1172
+ this.onMinDomainReached(false);
1173
+ }
1174
+
1175
+ return true;
1176
+ },
1177
+
1178
+ /**
1179
+ * Shift the calendar one domain backward
1180
+ *
1181
+ * The previous domain is loaded only if it's not beyond the minDate
1182
+ *
1183
+ * @return bool True if the previous domain was loaded, else false
1184
+ */
1185
+ loadPreviousDomain: function() {
1186
+ if (this._minDomainReached || this.minDomainIsReached(this._domains[0])) {
1187
+ return false;
1188
+ }
1189
+
1190
+ var previousDomainStartTimestamp = this.getPreviousDomain().getTime();
1191
+
1192
+ var parent = this;
1193
+ this._domains.unshift(previousDomainStartTimestamp);
1194
+ this._domains.pop();
1195
+
1196
+ this.paint(this.NAVIGATE_LEFT);
1197
+
1198
+ this.getDatas(
1199
+ 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);
1204
+ }
1205
+ );
1206
+
1207
+ this.afterLoadPreviousDomain(new Date(this._domains[0]));
1208
+
1209
+ if (this.minDomainIsReached(previousDomainStartTimestamp)) {
1210
+ this.onMinDomainReached(true);
1211
+ }
1212
+
1213
+ // Try to "disengage" the max domain reached setting
1214
+ if (this._maxDomainReached && !this.maxDomainIsReached(this._domains[this._domains.length-1])) {
1215
+ this.onMaxDomainReached(false);
1216
+ }
1217
+
1218
+ return true;
1219
+ },
1220
+
1221
+ /**
1222
+ * Return whether a date is inside the scope determined by maxDate
1223
+ *
1224
+ * @return bool
1225
+ */
1226
+ maxDomainIsReached: function(datetimestamp) {
1227
+ return (this.options.maxDate !== null && (this.options.maxDate.getTime() < datetimestamp));
1228
+ },
1229
+
1230
+ /**
1231
+ * Return whether a date is inside the scope determined by minDate
1232
+ *
1233
+ * @return bool
1234
+ */
1235
+ minDomainIsReached: function (datetimestamp) {
1236
+ return (this.options.minDate !== null && (this.options.minDate.getTime() >= datetimestamp));
1237
+ },
1238
+
1239
+ // =========================================================================//
1240
+ // PAINTING : LEGEND //
1241
+ // =========================================================================//
1242
+
1243
+ displayLegend: function(width) {
1244
+
1245
+ var parent = this;
1246
+ var legend = this.root;
1247
+
1248
+ switch(this.options.legendVerticalPosition) {
1249
+ case "top" : legend = legend.insert("svg", ".graph"); break;
1250
+ default : legend = legend.append("svg");
1251
+ }
1252
+
1253
+ var legendWidth =
1254
+ this.options.legendCellSize * (this.options.legend.length+1) +
1255
+ this.options.legendCellPadding * (this.options.legend.length+1) +
1256
+ this.options.legendMargin[3] + this.options.legendMargin[1];
1257
+
1258
+ legend = legend
1259
+ .attr("class", "graph-legend")
1260
+ .attr("height", this.options.legendCellSize + this.options.legendMargin[0] + this.options.legendMargin[2])
1261
+ .attr("width", width)
1262
+ .append("g")
1263
+ .attr("transform", function(d) {
1264
+ switch(parent.options.legendHorizontalPosition) {
1265
+ case "right" : return "translate(" + (width - legendWidth) + ")";
1266
+ case "middle" :
1267
+ case "center" : return "translate(" + (width/2 - legendWidth/2) + ")";
1268
+ default : return "translate(" + parent.options.legendMargin[3] + ")";
1269
+ }
1270
+ })
1271
+ .attr("y", this.options.legendMargin[0])
1272
+ .selectAll().data(d3.range(0, this.options.legend.length+1));
1273
+
1274
+ var legendItem = legend
1275
+ .enter()
1276
+ .append("rect")
1277
+ .attr("width", this.options.legendCellSize)
1278
+ .attr("height", this.options.legendCellSize)
1279
+ .attr("class", function(d){ return "graph-rect q" + (d+1); })
1280
+ .attr("x", function(d) {
1281
+ return d * (parent.options.legendCellSize + parent.options.legendCellPadding);
1282
+ })
1283
+ .attr("y", this.options.legendMargin[0])
1284
+ .attr("fill-opacity", 0)
1285
+ ;
1286
+
1287
+ legendItem.transition().delay(function(d, i) { return parent.options.animationDuration * i/10;}).attr("fill-opacity", 1);
1288
+
1289
+ legendItem
1290
+ .append("title")
1291
+ .text(function(d) {
1292
+ var nextThreshold = parent.options.legend[d+1];
1293
+ if (d === 0) {
1294
+ return (parent.options.legendTitleFormat.lower).format({
1295
+ min: parent.options.legend[d],
1296
+ name: parent.options.itemName[1]});
1297
+ } else if (d === parent.options.legend.length) {
1298
+ return (parent.options.legendTitleFormat.upper).format({
1299
+ max: parent.options.legend[d-1],
1300
+ name: parent.options.itemName[1]});
1301
+ } else {
1302
+ return (parent.options.legendTitleFormat.inner).format({
1303
+ down: parent.options.legend[d-1],
1304
+ up: parent.options.legend[d],
1305
+ name: parent.options.itemName[1]});
1306
+ }
1307
+ })
1308
+ ;
1309
+
1310
+ },
1311
+
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
+ // =========================================================================//
1390
+ // POSITIONNING //
1391
+ // =========================================================================//
1392
+
1393
+ positionSubDomainX: function(d) {
1394
+ var index = this._domainType[this.options.subDomain].position.x(d);
1395
+ return index * this.options.cellSize + index * this.options.cellPadding;
1396
+ },
1397
+
1398
+ positionSubDomainY: function(d) {
1399
+ var index = this._domainType[this.options.subDomain].position.y(d);
1400
+ return index * this.options.cellSize + index * this.options.cellPadding;
1401
+ },
1402
+
1403
+ /**
1404
+ * Return a classname if the specified date should be highlighted
1405
+ *
1406
+ * @param Date d a date
1407
+ * @return String the highlight class
1408
+ */
1409
+ getHighlightClassName: function(d)
1410
+ {
1411
+ if (this.options.highlight.length > 0) {
1412
+ for (var i in this.options.highlight) {
1413
+ if (this.options.highlight[i] instanceof Date && this.dateIsEqual(this.options.highlight[i], d)) {
1414
+ return " highlight" + (this.isNow(this.options.highlight[i]) ? " now" : "");
1415
+ }
1416
+ }
1417
+ }
1418
+ return "";
1419
+ },
1420
+
1421
+ /**
1422
+ * Return whether the specified date is now,
1423
+ * according to the type of subdomain
1424
+ *
1425
+ * @param Date d The date to compare
1426
+ * @return bool True if the date correspond to a subdomain cell
1427
+ */
1428
+ isNow: function(d) {
1429
+ return this.dateIsEqual(d, new Date());
1430
+ },
1431
+
1432
+ /**
1433
+ * Return whether 2 dates are equals
1434
+ * This function is subdomain-aware,
1435
+ * and dates comparison are dependent of the subdomain
1436
+ *
1437
+ * @param Date date_a First date to compare
1438
+ * @param Date date_b Secon date to compare
1439
+ * @return bool true if the 2 dates are equals
1440
+ */
1441
+ dateIsEqual: function(date_a, date_b) {
1442
+ switch(this.options.subDomain) {
1443
+ case "x_min" :
1444
+ case "min" :
1445
+ return date_a.getFullYear() === date_b.getFullYear() &&
1446
+ date_a.getMonth() === date_b.getMonth() &&
1447
+ date_a.getDate() === date_b.getDate() &&
1448
+ date_a.getHours() === date_b.getHours() &&
1449
+ date_a.getMinutes() === date_b.getMinutes();
1450
+ case "x_hour" :
1451
+ case "hour" :
1452
+ return date_a.getFullYear() === date_b.getFullYear() &&
1453
+ date_a.getMonth() === date_b.getMonth() &&
1454
+ date_a.getDate() === date_b.getDate() &&
1455
+ date_a.getHours() === date_b.getHours();
1456
+ case "x_day" :
1457
+ case "day" :
1458
+ return date_a.getFullYear() === date_b.getFullYear() &&
1459
+ date_a.getMonth() === date_b.getMonth() &&
1460
+ date_a.getDate() === date_b.getDate();
1461
+ case "x_week" :
1462
+ case "week" :
1463
+ case "x_month" :
1464
+ case "month" :
1465
+ return date_a.getFullYear() === date_b.getFullYear() &&
1466
+ date_a.getMonth() === date_b.getMonth();
1467
+ default : return false;
1468
+ }
1469
+ },
1470
+
1471
+ // =========================================================================//
1472
+ // DOMAIN COMPUTATION //
1473
+ // =========================================================================//
1474
+
1475
+ /**
1476
+ * Return the day of the year for the date
1477
+ * @param Date
1478
+ * @return int Day of the year [1,366]
1479
+ */
1480
+ getDayOfYear : d3.time.format("%j"),
1481
+
1482
+ /**
1483
+ * Return the week number of the year
1484
+ * Monday as the first day of the week
1485
+ * @return int Week number [0-53]
1486
+ */
1487
+ getWeekNumber : function(d) {
1488
+ var f = this.options.weekStartOnMonday === true ? d3.time.format("%W") : d3.time.format("%U");
1489
+ return f(d);
1490
+ },
1491
+
1492
+
1493
+ getWeekDay : function(d) {
1494
+ if (this.options.weekStartOnMonday === false) {
1495
+ return d.getDay();
1496
+ }
1497
+ else if (d.getDay() === 0) {
1498
+ return 6;
1499
+ }
1500
+ return d.getDay()-1;
1501
+ },
1502
+
1503
+
1504
+ /**
1505
+ * Get the last day of the month
1506
+ * @param Date|int d Date or timestamp in milliseconds
1507
+ * @return Date Last day of the month
1508
+ */
1509
+ getEndOfMonth : function(d) {
1510
+ if (typeof d === "number") {
1511
+ d = new Date(d);
1512
+ }
1513
+ return new Date(d.getFullYear(), d.getMonth()+1, 0);
1514
+ },
1515
+
1516
+ /**
1517
+ * Return a range of week number
1518
+ * @param number|Date d A date, or timestamp in milliseconds
1519
+ * @return Date The start of the hour
1520
+ */
1521
+ getWeekDomain: function (d, range) {
1522
+ var weekStart;
1523
+
1524
+ if (this.options.weekStartOnMonday === false) {
1525
+ weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate() - d.getDay());
1526
+ } else {
1527
+ if (d.getDay() === 1) {
1528
+ weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate());
1529
+ } else if (d.getDay() === 0) {
1530
+ weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate());
1531
+ weekStart.setDate(weekStart.getDate() - 6);
1532
+ } else {
1533
+ weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()-d.getDay()+1);
1534
+ }
1535
+ }
1536
+
1537
+ var endDate = new Date(weekStart);
1538
+
1539
+ var stop = new Date(endDate.setDate(endDate.getDate() + range * 7));
1540
+
1541
+ return (this.options.weekStartOnMonday === true) ?
1542
+ d3.time.mondays(Math.min(weekStart, stop), Math.max(weekStart, stop)) :
1543
+ d3.time.sundays(Math.min(weekStart, stop), Math.max(weekStart, stop))
1544
+ ;
1545
+ },
1546
+
1547
+ getYearDomain: function(d, range){
1548
+ var start = new Date(d.getFullYear(), 0);
1549
+ var stop = new Date(d.getFullYear()+range, 0);
1550
+
1551
+ return d3.time.years(Math.min(start, stop), Math.max(start, stop));
1552
+ },
1553
+
1554
+ /**
1555
+ * Return all the minutes between from the same hour
1556
+ * @param number|Date d A date, or timestamp in milliseconds
1557
+ * @return Date The start of the hour
1558
+ */
1559
+ getMinuteDomain: function (d, range) {
1560
+ var start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours());
1561
+ var stop = new Date(start.getTime() + 60 * 1000 * range);
1562
+
1563
+ return d3.time.minutes(Math.min(start, stop), Math.max(start, stop));
1564
+ },
1565
+
1566
+ /**
1567
+ * Return the start of an hour
1568
+ * @param number|Date d A date, or timestamp in milliseconds
1569
+ * @return Date The start of the hour
1570
+ */
1571
+ getHourDomain: function (d, range) {
1572
+ var start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours());
1573
+ var stop = range;
1574
+ if (typeof range === "number") {
1575
+ stop = new Date(start.getTime() + 3600 * 1000 * range);
1576
+ }
1577
+
1578
+ return d3.time.hours(Math.min(start, stop), Math.max(start, stop));
1579
+ },
1580
+
1581
+ /**
1582
+ * Return the start of an hour
1583
+ * @param number|Date d A date, or timestamp in milliseconds
1584
+ * @param int range Number of days in the range
1585
+ * @return Date The start of the hour
1586
+ */
1587
+ getDayDomain: function (d, range) {
1588
+ var start = new Date(d.getFullYear(), d.getMonth(), d.getDate());
1589
+ var stop = new Date(start);
1590
+ stop = new Date(stop.setDate(stop.getDate() + parseInt(range, 10)));
1591
+
1592
+ return d3.time.days(Math.min(start, stop), Math.max(start, stop));
1593
+ },
1594
+
1595
+ /**
1596
+ * Return the month domain for the current date
1597
+ * @param Date d A date
1598
+ * @return Array
1599
+ */
1600
+ getMonthDomain: function (d, range) {
1601
+ var start = new Date(d.getFullYear(), d.getMonth());
1602
+ var stop = new Date(start);
1603
+ stop = stop.setMonth(stop.getMonth()+range);
1604
+
1605
+ return d3.time.months(Math.min(start, stop), Math.max(start, stop));
1606
+ },
1607
+
1608
+ getDomain: function(date, range) {
1609
+ if (typeof date === "number") {
1610
+ date = new Date(date);
1611
+ }
1612
+
1613
+ if (typeof range === "undefined") {
1614
+ range = this.options.range;
1615
+ }
1616
+
1617
+ switch(this.options.domain) {
1618
+ case "hour" : return this.getHourDomain(date, range);
1619
+ case "day" : return this.getDayDomain(date, range);
1620
+ case "week" : return this.getWeekDomain(date, range);
1621
+ case "month" : return this.getMonthDomain(date, range);
1622
+ case "year" : return this.getYearDomain(date, range);
1623
+ }
1624
+ },
1625
+
1626
+ getSubDomain: function(date) {
1627
+ if (typeof date === "number") {
1628
+ date = new Date(date);
1629
+ }
1630
+
1631
+ var parent = this;
1632
+
1633
+ var computeDaySubDomainSize = function(date, domain) {
1634
+ switch(domain) {
1635
+ case "year" : return parent.getDayOfYear(new Date(date.getFullYear()+1, 0, 0));
1636
+ case "month" :
1637
+ var lastDayOfMonth = new Date(date.getFullYear(), date.getMonth()+1, 0);
1638
+ return lastDayOfMonth.getDate();
1639
+ case "week" : return 7;
1640
+ }
1641
+ };
1642
+
1643
+ var computeMinSubDomainSize = function(date, domain) {
1644
+ switch (domain) {
1645
+ case "hour" : return 60;
1646
+ case "day" : return 60 * 24;
1647
+ case "week" : return 60 * 24 * 7;
1648
+ }
1649
+ };
1650
+
1651
+ var computeHourSubDomainSize = function(date, domain) {
1652
+ switch(domain) {
1653
+ case "day" : return 24;
1654
+ case "week" : return 168;
1655
+ case "month" :
1656
+ var endOfMonth = new Date(date.getFullYear(), date.getMonth()+1, 0);
1657
+ return endOfMonth.getDate() * 24;
1658
+ }
1659
+ };
1660
+
1661
+ var computeWeekSubDomainSize = function(date, domain) {
1662
+ if (domain === "month") {
1663
+ var endOfMonth = new Date(date.getFullYear(), date.getMonth()+1, 0);
1664
+ var endWeekNb = parent.getWeekNumber(endOfMonth);
1665
+ var startWeekNb = parent.getWeekNumber(new Date(date.getFullYear(), date.getMonth()));
1666
+
1667
+ if (startWeekNb > endWeekNb) {
1668
+ startWeekNb = 0;
1669
+ endWeekNb++;
1670
+ }
1671
+
1672
+ return endWeekNb - startWeekNb + 1;
1673
+ } else if (domain === "year") {
1674
+ return parent.getWeekNumber(new Date(date.getFullYear(), 11, 31));
1675
+ }
1676
+ };
1677
+
1678
+
1679
+ switch(this.options.subDomain) {
1680
+ case "x_min" :
1681
+ case "min" : return this.getMinuteDomain(date, computeMinSubDomainSize(date, this.options.domain));
1682
+ case "x_hour":
1683
+ case "hour" : return this.getHourDomain(date, computeHourSubDomainSize(date, this.options.domain));
1684
+ case "x_day" :
1685
+ case "day" : return this.getDayDomain(date, computeDaySubDomainSize(date, this.options.domain));
1686
+ case "week" : return this.getWeekDomain(date, computeWeekSubDomainSize(date, this.options.domain));
1687
+ case "x_month":
1688
+ case "month" : return this.getMonthDomain(date, 12);
1689
+ }
1690
+ },
1691
+
1692
+ getNextDomain: function() {
1693
+ return this.getDomain(this._domains[this._domains.length-1], 2).pop();
1694
+ },
1695
+
1696
+ getPreviousDomain: function() {
1697
+ return this.getDomain(this._domains[0], -1)[0];
1698
+ },
1699
+
1700
+ /**
1701
+ * Return the classname on the legend for the specified value
1702
+ *
1703
+ * @param Item count n Number of items for that perdiod of time
1704
+ * @return string Classname according to the legend
1705
+ */
1706
+ legend: function(n) {
1707
+
1708
+ if (isNaN(n)) {
1709
+ return "qi";
1710
+ } else if (n === null) {
1711
+ return "";
1712
+ }
1713
+
1714
+ for (var i = 0, total = this.options.legend.length-1; i <= total; i++) {
1715
+
1716
+ if (n === 0 && this.options.legend[0] > 0) {
1717
+ return "";
1718
+ } else if (this.options.legend[0] > 0 && n < 0) {
1719
+ return "qi";
1720
+ }
1721
+
1722
+ if (n <= this.options.legend[i]) {
1723
+ return "q" + (i+1);
1724
+ }
1725
+ }
1726
+ return "q" + (this.options.legend.length + 1);
1727
+ },
1728
+
1729
+ // =========================================================================//
1730
+ // DATAS //
1731
+ // =========================================================================//
1732
+
1733
+ /**
1734
+ * @todo Add check for empty data
1735
+ *
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
1746
+ *
1747
+ * @return mixed
1748
+ * - True if no data to load
1749
+ * - False if data is loaded asynchornously
1750
+ * - json object
1751
+ */
1752
+ getDatas: function(source, startDate, endDate, callback) {
1753
+ var parent = this;
1754
+
1755
+ switch(typeof source) {
1756
+ case "string" :
1757
+ if (source === "") {
1758
+ this.onComplete();
1759
+ return true;
1760
+ } else {
1761
+
1762
+ switch(this.options.dataType) {
1763
+ case "json" :
1764
+ d3.json(this.parseURI(source, startDate, endDate), callback);
1765
+ break;
1766
+ case "csv" :
1767
+ d3.csv(this.parseURI(source, startDate, endDate), callback);
1768
+ break;
1769
+ case "tsv" :
1770
+ d3.tsv(this.parseURI(source, startDate, endDate), callback);
1771
+ break;
1772
+ case "text" :
1773
+ d3.text(this.parseURI(source, startDate, endDate), "text/plain", callback);
1774
+ break;
1775
+ }
1776
+
1777
+ return false;
1778
+ }
1779
+ break;
1780
+ case "object" :
1781
+ // @todo Check that it's a valid JSON object
1782
+ callback(source);
1783
+ }
1784
+
1785
+ return true;
1786
+ },
1787
+
1788
+ /**
1789
+ * Convert a JSON result into the expected format
1790
+ *
1791
+ * @param {[type]} data [description]
1792
+ * @return {[type]} [description]
1793
+ */
1794
+ parseDatas: function(data) {
1795
+ var stats = {};
1796
+
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 {};
1802
+ }
1803
+
1804
+ for (var d in data) {
1805
+ var date = new Date(d*1000);
1806
+ var domainUnit = this.getDomain(date)[0].getTime();
1807
+
1808
+ // Don't record datas not relevant to the current domain
1809
+ if (this._domains.indexOf(domainUnit) < 0) {
1810
+ continue;
1811
+ }
1812
+
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];
1822
+ }
1823
+ }
1824
+
1825
+ return stats;
1826
+ },
1827
+
1828
+ parseURI: function(str, startDate, endDate) {
1829
+ // Use a timestamp in seconds
1830
+ str = str.replace(/\{\{t:start\}\}/g, startDate.getTime()/1000);
1831
+ str = str.replace(/\{\{t:end\}\}/g, endDate.getTime()/1000);
1832
+
1833
+ // Use a string date, following the ISO-8601
1834
+ str = str.replace(/\{\{d:start\}\}/g, startDate.toISOString());
1835
+ str = str.replace(/\{\{d:end\}\}/g, endDate.toISOString());
1836
+
1837
+ return str;
1838
+ },
1839
+
1840
+ // =========================================================================//
1841
+ // PUBLIC API //
1842
+ // =========================================================================//
1843
+
1844
+ next: function() {
1845
+ return this.loadNextDomain();
1846
+ },
1847
+
1848
+ previous: function() {
1849
+ return this.loadPreviousDomain();
1850
+ },
1851
+
1852
+ getSVG: function() {
1853
+ var styles = {
1854
+ ".graph": {},
1855
+ ".graph-rect": {},
1856
+ "rect.highlight": {},
1857
+ "rect.now": {},
1858
+ "text.highlight": {},
1859
+ "text.now": {},
1860
+ ".domain-background": {},
1861
+ ".graph-label": {},
1862
+ ".subdomain-text": {},
1863
+ ".qi": {}
1864
+ };
1865
+
1866
+ for (var j = 0, total = this.options.legend.length; j < total; j++) {
1867
+ styles[".q" + j] = {};
1868
+ }
1869
+
1870
+ var root = this.root;
1871
+
1872
+ var whitelistStyles = [
1873
+ // SVG specific properties
1874
+ "stroke", "stroke-width", "stroke-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-miterlimit",
1875
+ "fill", "fill-opacity", "fill-rule",
1876
+ "marker", "marker-start", "marker-mid", "marker-end",
1877
+ "alignement-baseline", "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal", "glyph-orientation-vertical", "kerning", "text-anchor",
1878
+ "shape-rendering",
1879
+
1880
+ // Text Specific properties
1881
+ "text-transform", "font-family", "font", "font-size", "font-weight"
1882
+ ];
1883
+
1884
+ var filterStyles = function(attribute, property, value) {
1885
+ if (whitelistStyles.indexOf(property) !== -1) {
1886
+ styles[attribute][property] = value;
1887
+ }
1888
+ };
1889
+
1890
+ var getElement = function(e) {
1891
+ return root.select(e)[0][0];
1892
+ };
1893
+
1894
+ for (var element in styles) {
1895
+
1896
+ var dom = getElement(element);
1897
+
1898
+ if (dom === null) {
1899
+ continue;
1900
+ }
1901
+
1902
+ // The DOM Level 2 CSS way
1903
+ if ("getComputedStyle" in window) {
1904
+ var cs = getComputedStyle(dom, null);
1905
+ if (cs.length !== 0) {
1906
+ for (var i = 0; i < cs.length; i++) {
1907
+ filterStyles(element, cs.item(i), cs.getPropertyValue(cs.item(i)));
1908
+ }
1909
+
1910
+ // Opera workaround. Opera doesn"t support `item`/`length`
1911
+ // on CSSStyleDeclaration.
1912
+ } else {
1913
+ for (var k in cs) {
1914
+ if (cs.hasOwnProperty(k)) {
1915
+ filterStyles(element, k, cs[k]);
1916
+ }
1917
+ }
1918
+ }
1919
+
1920
+ // The IE way
1921
+ } else if ("currentStyle" in dom) {
1922
+ var css = dom.currentStyle;
1923
+ for (var p in css) {
1924
+ filterStyles(element, p, css[p]);
1925
+ }
1926
+ }
1927
+ }
1928
+
1929
+
1930
+
1931
+ var string = "<svg xmlns=\"http://www.w3.org/2000/svg\" "+
1932
+ "xmlns:xlink=\"http://www.w3.org/1999/xlink\"><style type=\"text/css\"><![CDATA[ ";
1933
+
1934
+ for (var style in styles) {
1935
+ string += style + " {\n";
1936
+ for (var l in styles[style]) {
1937
+ string += "\t" + l + ":" + styles[style][l] + ";\n";
1938
+ }
1939
+ string += "}\n";
1940
+ }
1941
+
1942
+ string += "]]></style>";
1943
+ string += new XMLSerializer().serializeToString(this.root.selectAll("svg")[0][0]);
1944
+ string += new XMLSerializer().serializeToString(this.root.selectAll("svg")[0][1]);
1945
+ string += "</svg>";
1946
+
1947
+ return string;
1948
+ }
1949
+ };
1950
+
1951
+ var DomainPosition = function() {
1952
+ this.positions = [];
1953
+ };
1954
+
1955
+ DomainPosition.prototype.getPosition = function(i) {
1956
+ return this.positions[i];
1957
+ };
1958
+
1959
+ DomainPosition.prototype.getLast = function() {
1960
+ return this.positions[this.positions.length-1];
1961
+ };
1962
+
1963
+ DomainPosition.prototype.pushPosition = function(dim) {
1964
+ this.positions.push(dim);
1965
+ };
1966
+
1967
+ DomainPosition.prototype.unshiftPosition = function(dim) {
1968
+ this.positions.unshift(dim);
1969
+ };
1970
+
1971
+ DomainPosition.prototype.shiftRight = function(exitingDomainDim) {
1972
+ for(var i in this.positions) {
1973
+ this.positions[i] -= exitingDomainDim;
1974
+ }
1975
+ this.positions.shift();
1976
+ };
1977
+
1978
+ DomainPosition.prototype.shiftLeft = function(enteringDomainDim) {
1979
+ for(var i in this.positions) {
1980
+ this.positions[i] += enteringDomainDim;
1981
+ }
1982
+ this.positions.pop();
1983
+ };
1984
+
1985
+
1986
+ /**
1987
+ * Sprintf like function
1988
+ * @source http://stackoverflow.com/a/4795914/805649
1989
+ * @return String
1990
+ */
1991
+ String.prototype.format = function () {
1992
+ var formatted = this;
1993
+ for (var prop in arguments[0]) {
1994
+ var regexp = new RegExp("\\{" + prop + "\\}", "gi");
1995
+ formatted = formatted.replace(regexp, arguments[0][prop]);
1996
+ }
1997
+ return formatted;
1998
+ };
1999
+
2000
+ /**
2001
+ * #source http://stackoverflow.com/a/383245/805649
2002
+ */
2003
+ function mergeRecursive(obj1, obj2) {
2004
+
2005
+ for (var p in obj2) {
2006
+ try {
2007
+ // Property in destination object set; update its value.
2008
+ if (obj2[p].constructor === Object) {
2009
+ obj1[p] = mergeRecursive(obj1[p], obj2[p]);
2010
+ } else {
2011
+ obj1[p] = obj2[p];
2012
+ }
2013
+ } catch(e) {
2014
+ // Property in destination object not set; create it and set its value.
2015
+ obj1[p] = obj2[p];
2016
+ }
2017
+ }
2018
+
2019
+ return obj1;
2020
+ }
2021
+
2022
+ /**
2023
+ * AMD Loader
2024
+ */
2025
+ if (typeof define === "function" && define.amd) {
2026
+ define(["d3"], function(d3) {
2027
+ return CalHeatMap;
2028
+ });
2029
+ }