cal-heatmap-rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c3cdc640e7ab9403e6e87c99d6030552a40e1bd3
4
+ data.tar.gz: 67aebdb6f237126920c1db7a8c067ad803ff958b
5
+ SHA512:
6
+ metadata.gz: 67014a70a85cfe7dc6f7b9bd20ce84173aef11c5639f2fe875312fe586718bd629f3df1e059b188837ddbc7f2ec0032da9d723f39534554543bbdea00d3512d5
7
+ data.tar.gz: 261ac16d8eae83359da69c582452119209abe631eb2af5dc43bab5f2da80d0e8736ecc2d472147eca08546c064e578a8aa454c63564dfeb78c4c1481a4e0cb4c
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Pavol Zbell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,59 @@
1
+ # Cal-HeatMap Rails
2
+
3
+ Packages [Cal-HeatMap](https://github.com/kamisama/cal-heatmap) for Rails Asser Pipeline.
4
+
5
+ - [D3](https://github.com/mbostock/d3) 3.4.6 (required, not included)
6
+ - [Cal-HeatMap](https://github.com/kamisama/cal-heatmap) 3.4.0 (included)
7
+
8
+ ## Installation
9
+
10
+ gem 'd3_rails'
11
+ gem 'cal-heatmap-rails', github: 'pavolzbell/cal-heatmap-rails', branch: :master
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install cal_heatmap_rails
20
+
21
+ ## Usage
22
+
23
+ Include in your `application.js` manifest:
24
+
25
+ ```
26
+ //= require d3
27
+ //= require cal-heatmap
28
+ ```
29
+
30
+ and in your `application.css` manifest:
31
+
32
+ ```
33
+ *= require cal-heatmap
34
+ ```
35
+
36
+ ## Included Javascripts
37
+
38
+ cal-heatmap.js
39
+ cal-heatmap-min.js
40
+
41
+ ## Included Stylesheets
42
+
43
+ cal-heatmap.css
44
+
45
+ ## Testing
46
+
47
+ Go to `spec/dummy` and run `bundle`. After bundling, run specs with `bundle exec rspec`.
48
+
49
+ ## Contributing
50
+
51
+ 1. Fork it
52
+ 2. Create your feature branch (`git checkout -b new-feature`)
53
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
54
+ 4. Push to the branch (`git push origin new-feature`)
55
+ 5. Create new Pull Request
56
+
57
+ ## License
58
+
59
+ This software is released under the [MIT License](LICENSE.md).
@@ -0,0 +1,10 @@
1
+ require 'cal/heatmap/rails/version'
2
+
3
+ module Cal
4
+ module Heatmap
5
+ module Rails
6
+ class Engine < ::Rails::Engine
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module Cal
2
+ module Heatmap
3
+ module Rails
4
+ VERSION = '0.0.1'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3412 @@
1
+ /*! cal-heatmap v3.4.0 (Sun Feb 02 2014 13:03:14)
2
+ * ---------------------------------------------
3
+ * Cal-Heatmap is a javascript module to create calendar heatmap to visualize time series data
4
+ * https://github.com/kamisama/cal-heatmap
5
+ * Licensed under the MIT license
6
+ * Copyright 2014 Wan Qi Chen
7
+ */
8
+
9
+ var CalHeatMap = function() {
10
+ "use strict";
11
+
12
+ var self = this;
13
+
14
+ this.allowedDataType = ["json", "csv", "tsv", "txt"];
15
+
16
+ // Default settings
17
+ this.options = {
18
+ // selector string of the container to append the graph to
19
+ // Accept any string value accepted by document.querySelector or CSS3
20
+ // or an Element object
21
+ itemSelector: "#cal-heatmap",
22
+
23
+ // Whether to paint the calendar on init()
24
+ // Used by testsuite to reduce testing time
25
+ paintOnLoad: true,
26
+
27
+ // ================================================
28
+ // DOMAIN
29
+ // ================================================
30
+
31
+ // Number of domain to display on the graph
32
+ range: 12,
33
+
34
+ // Size of each cell, in pixel
35
+ cellSize: 10,
36
+
37
+ // Padding between each cell, in pixel
38
+ cellPadding: 2,
39
+
40
+ // For rounded subdomain rectangles, in pixels
41
+ cellRadius: 0,
42
+
43
+ domainGutter: 2,
44
+
45
+ domainMargin: [0, 0, 0, 0],
46
+
47
+ domain: "hour",
48
+
49
+ subDomain: "min",
50
+
51
+ // Number of columns to split the subDomains to
52
+ // If not null, will takes precedence over rowLimit
53
+ colLimit: null,
54
+
55
+ // Number of rows to split the subDomains to
56
+ // Will be ignored if colLimit is not null
57
+ rowLimit: null,
58
+
59
+ // First day of the week is Monday
60
+ // 0 to start the week on Sunday
61
+ weekStartOnMonday: true,
62
+
63
+ // Start date of the graph
64
+ // @default now
65
+ start: new Date(),
66
+
67
+ minDate: null,
68
+
69
+ maxDate: null,
70
+
71
+ // URL, where to fetch the original datas
72
+ data: "",
73
+
74
+ dataType: this.allowedDataType[0],
75
+
76
+ // Whether to consider missing date:value from the datasource
77
+ // as equal to 0, or just leave them as missing
78
+ considerMissingDataAsZero: false,
79
+
80
+ // Load remote data on calendar creation
81
+ // When false, the calendar will be left empty
82
+ loadOnInit: true,
83
+
84
+ // Calendar orientation
85
+ // false: display domains side by side
86
+ // true : display domains one under the other
87
+ verticalOrientation: false,
88
+
89
+ // Domain dynamic width/height
90
+ // The width on a domain depends on the number of
91
+ domainDynamicDimension: true,
92
+
93
+ // Domain Label properties
94
+ label: {
95
+ // valid: top, right, bottom, left
96
+ position: "bottom",
97
+
98
+ // Valid: left, center, right
99
+ // Also valid are the direct svg values: start, middle, end
100
+ align: "center",
101
+
102
+ // By default, there is no margin/padding around the label
103
+ offset: {
104
+ x: 0,
105
+ y: 0
106
+ },
107
+
108
+ rotate: null,
109
+
110
+ // Used only on vertical orientation
111
+ width: 100,
112
+
113
+ // Used only on horizontal orientation
114
+ height: null
115
+ },
116
+
117
+ // ================================================
118
+ // LEGEND
119
+ // ================================================
120
+
121
+ // Threshold for the legend
122
+ legend: [10, 20, 30, 40],
123
+
124
+ // Whether to display the legend
125
+ displayLegend: true,
126
+
127
+ legendCellSize: 10,
128
+
129
+ legendCellPadding: 2,
130
+
131
+ legendMargin: [0, 0, 0, 0],
132
+
133
+ // Legend vertical position
134
+ // top: place legend above calendar
135
+ // bottom: place legend below the calendar
136
+ legendVerticalPosition: "bottom",
137
+
138
+ // Legend horizontal position
139
+ // accepted values: left, center, right
140
+ legendHorizontalPosition: "left",
141
+
142
+ // Legend rotation
143
+ // accepted values: horizontal, vertical
144
+ legendOrientation: "horizontal",
145
+
146
+ // Objects holding all the heatmap different colors
147
+ // null to disable, and use the default css styles
148
+ //
149
+ // Examples:
150
+ // legendColors: {
151
+ // min: "green",
152
+ // max: "red",
153
+ // empty: "#ffffff",
154
+ // base: "grey",
155
+ // overflow: "red"
156
+ // }
157
+ legendColors: null,
158
+
159
+ // ================================================
160
+ // HIGHLIGHT
161
+ // ================================================
162
+
163
+ // List of dates to highlight
164
+ // Valid values:
165
+ // - []: don't highlight anything
166
+ // - "now": highlight the current date
167
+ // - an array of Date objects: highlight the specified dates
168
+ highlight: [],
169
+
170
+ // ================================================
171
+ // TEXT FORMATTING / i18n
172
+ // ================================================
173
+
174
+ // Name of the items to represent in the calendar
175
+ itemName: ["item", "items"],
176
+
177
+ // Formatting of the domain label
178
+ // @default: null, will use the formatting according to domain type
179
+ // Accept a string used as specifier by d3.time.format()
180
+ // or a function
181
+ //
182
+ // Refer to https://github.com/mbostock/d3/wiki/Time-Formatting
183
+ // for accepted date formatting used by d3.time.format()
184
+ domainLabelFormat: null,
185
+
186
+ // Formatting of the title displayed when hovering a subDomain cell
187
+ subDomainTitleFormat: {
188
+ empty: "{date}",
189
+ filled: "{count} {name} {connector} {date}"
190
+ },
191
+
192
+ // Formatting of the {date} used in subDomainTitleFormat
193
+ // @default: null, will use the formatting according to subDomain type
194
+ // Accept a string used as specifier by d3.time.format()
195
+ // or a function
196
+ //
197
+ // Refer to https://github.com/mbostock/d3/wiki/Time-Formatting
198
+ // for accepted date formatting used by d3.time.format()
199
+ subDomainDateFormat: null,
200
+
201
+ // Formatting of the text inside each subDomain cell
202
+ // @default: null, no text
203
+ // Accept a string used as specifier by d3.time.format()
204
+ // or a function
205
+ //
206
+ // Refer to https://github.com/mbostock/d3/wiki/Time-Formatting
207
+ // for accepted date formatting used by d3.time.format()
208
+ subDomainTextFormat: null,
209
+
210
+ // Formatting of the title displayed when hovering a legend cell
211
+ legendTitleFormat: {
212
+ lower: "less than {min} {name}",
213
+ inner: "between {down} and {up} {name}",
214
+ upper: "more than {max} {name}"
215
+ },
216
+
217
+ // Animation duration, in ms
218
+ animationDuration: 500,
219
+
220
+ nextSelector: false,
221
+
222
+ previousSelector: false,
223
+
224
+ itemNamespace: "cal-heatmap",
225
+
226
+ tooltip: false,
227
+
228
+ // ================================================
229
+ // EVENTS CALLBACK
230
+ // ================================================
231
+
232
+ // Callback when clicking on a time block
233
+ onClick: null,
234
+
235
+ // Callback after painting the empty calendar
236
+ // Can be used to trigger an API call, once the calendar is ready to be filled
237
+ afterLoad: null,
238
+
239
+ // Callback after loading the next domain in the calendar
240
+ afterLoadNextDomain: null,
241
+
242
+ // Callback after loading the previous domain in the calendar
243
+ afterLoadPreviousDomain: null,
244
+
245
+ // Callback after finishing all actions on the calendar
246
+ onComplete: null,
247
+
248
+ // Callback after fetching the datas, but before applying them to the calendar
249
+ // Used mainly to convert the datas if they're not formatted like expected
250
+ // Takes the fetched "data" object as argument, must return a json object
251
+ // formatted like {timestamp:count, timestamp2:count2},
252
+ afterLoadData: function(data) { return data; },
253
+
254
+ // Callback triggered after calling next().
255
+ // The `status` argument is equal to true if there is no
256
+ // more next domain to load
257
+ //
258
+ // This callback is also executed once, after calling previous(),
259
+ // only when the max domain is reached
260
+ onMaxDomainReached: null,
261
+
262
+ // Callback triggered after calling previous().
263
+ // The `status` argument is equal to true if there is no
264
+ // more previous domain to load
265
+ //
266
+ // This callback is also executed once, after calling next(),
267
+ // only when the min domain is reached
268
+ onMinDomainReached: null
269
+ };
270
+
271
+ this._domainType = {
272
+ "min": {
273
+ name: "minute",
274
+ level: 10,
275
+ maxItemNumber: 60,
276
+ defaultRowNumber: 10,
277
+ defaultColumnNumber: 6,
278
+ row: function(d) { return self.getSubDomainRowNumber(d); },
279
+ column: function(d) { return self.getSubDomainColumnNumber(d); },
280
+ position: {
281
+ x: function(d) { return Math.floor(d.getMinutes() / self._domainType.min.row(d)); },
282
+ y: function(d) { return d.getMinutes() % self._domainType.min.row(d); }
283
+ },
284
+ format: {
285
+ date: "%H:%M, %A %B %-e, %Y",
286
+ legend: "",
287
+ connector: "at"
288
+ },
289
+ extractUnit: function(d) {
290
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes()).getTime();
291
+ }
292
+ },
293
+ "hour": {
294
+ name: "hour",
295
+ level: 20,
296
+ maxItemNumber: function(d) {
297
+ switch(self.options.domain) {
298
+ case "day":
299
+ return 24;
300
+ case "week":
301
+ return 24 * 7;
302
+ case "month":
303
+ return 24 * (self.options.domainDynamicDimension ? self.getDayCountInMonth(d): 31);
304
+ }
305
+ },
306
+ defaultRowNumber: 6,
307
+ defaultColumnNumber: function(d) {
308
+ switch(self.options.domain) {
309
+ case "day":
310
+ return 4;
311
+ case "week":
312
+ return 28;
313
+ case "month":
314
+ return self.options.domainDynamicDimension ? self.getDayCountInMonth(d): 31;
315
+ }
316
+ },
317
+ row: function(d) { return self.getSubDomainRowNumber(d); },
318
+ column: function(d) { return self.getSubDomainColumnNumber(d); },
319
+ position: {
320
+ x: function(d) {
321
+ if (self.options.domain === "month") {
322
+ if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
323
+ return Math.floor((d.getHours() + (d.getDate()-1)*24) / self._domainType.hour.row(d));
324
+ }
325
+ return Math.floor(d.getHours() / self._domainType.hour.row(d)) + (d.getDate()-1)*4;
326
+ } else if (self.options.domain === "week") {
327
+ if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
328
+ return Math.floor((d.getHours() + self.getWeekDay(d)*24) / self._domainType.hour.row(d));
329
+ }
330
+ return Math.floor(d.getHours() / self._domainType.hour.row(d)) + self.getWeekDay(d)*4;
331
+ }
332
+ return Math.floor(d.getHours() / self._domainType.hour.row(d));
333
+ },
334
+ y: function(d) {
335
+ var p = d.getHours();
336
+ if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
337
+ switch(self.options.domain) {
338
+ case "month":
339
+ p += (d.getDate()-1) * 24;
340
+ break;
341
+ case "week":
342
+ p += self.getWeekDay(d) * 24;
343
+ break;
344
+ }
345
+ }
346
+ return Math.floor(p % self._domainType.hour.row(d));
347
+ }
348
+ },
349
+ format: {
350
+ date: "%Hh, %A %B %-e, %Y",
351
+ legend: "%H:00",
352
+ connector: "at"
353
+ },
354
+ extractUnit: function(d) {
355
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()).getTime();
356
+ }
357
+ },
358
+ "day": {
359
+ name: "day",
360
+ level: 30,
361
+ maxItemNumber: function(d) {
362
+ switch(self.options.domain) {
363
+ case "week":
364
+ return 7;
365
+ case "month":
366
+ return self.options.domainDynamicDimension ? self.getDayCountInMonth(d) : 31;
367
+ case "year":
368
+ return self.options.domainDynamicDimension ? self.getDayCountInYear(d) : 366;
369
+ }
370
+ },
371
+ defaultColumnNumber: function(d) {
372
+ d = new Date(d);
373
+ switch(self.options.domain) {
374
+ case "week":
375
+ return 1;
376
+ case "month":
377
+ return (self.options.domainDynamicDimension && !self.options.verticalOrientation) ? (self.getWeekNumber(new Date(d.getFullYear(), d.getMonth()+1, 0)) - self.getWeekNumber(d) + 1): 6;
378
+ case "year":
379
+ return (self.options.domainDynamicDimension ? (self.getWeekNumber(new Date(d.getFullYear(), 11, 31)) - self.getWeekNumber(new Date(d.getFullYear(), 0)) + 1): 54);
380
+ }
381
+ },
382
+ defaultRowNumber: 7,
383
+ row: function(d) { return self.getSubDomainRowNumber(d); },
384
+ column: function(d) { return self.getSubDomainColumnNumber(d); },
385
+ position: {
386
+ x: function(d) {
387
+ switch(self.options.domain) {
388
+ case "week":
389
+ return Math.floor(self.getWeekDay(d) / self._domainType.day.row(d));
390
+ case "month":
391
+ if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
392
+ return Math.floor((d.getDate() - 1)/ self._domainType.day.row(d));
393
+ }
394
+ return self.getWeekNumber(d) - self.getWeekNumber(new Date(d.getFullYear(), d.getMonth()));
395
+ case "year":
396
+ if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
397
+ return Math.floor((self.getDayOfYear(d) - 1) / self._domainType.day.row(d));
398
+ }
399
+ return self.getWeekNumber(d);
400
+ }
401
+ },
402
+ y: function(d) {
403
+ var p = self.getWeekDay(d);
404
+ if (self.options.colLimit > 0 || self.options.rowLimit > 0) {
405
+ switch(self.options.domain) {
406
+ case "year":
407
+ p = self.getDayOfYear(d) - 1;
408
+ break;
409
+ case "week":
410
+ p = self.getWeekDay(d);
411
+ break;
412
+ case "month":
413
+ p = d.getDate() - 1;
414
+ break;
415
+ }
416
+ }
417
+ return Math.floor(p % self._domainType.day.row(d));
418
+ }
419
+ },
420
+ format: {
421
+ date: "%A %B %-e, %Y",
422
+ legend: "%e %b",
423
+ connector: "on"
424
+ },
425
+ extractUnit: function(d) {
426
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
427
+ }
428
+ },
429
+ "week": {
430
+ name: "week",
431
+ level: 40,
432
+ maxItemNumber: 54,
433
+ defaultColumnNumber: function(d) {
434
+ d = new Date(d);
435
+ switch(self.options.domain) {
436
+ case "year":
437
+ return self._domainType.week.maxItemNumber;
438
+ case "month":
439
+ return self.getWeekNumber(new Date(d.getFullYear(), d.getMonth()+1, 0)) - self.getWeekNumber(d);
440
+ }
441
+ },
442
+ defaultRowNumber: 1,
443
+ row: function(d) { return self.getSubDomainRowNumber(d); },
444
+ column: function(d) { return self.getSubDomainColumnNumber(d); },
445
+ position: {
446
+ x: function(d) {
447
+ switch(self.options.domain) {
448
+ case "year":
449
+ return Math.floor(self.getWeekNumber(d) / self._domainType.week.row(d));
450
+ case "month":
451
+ return Math.floor(self.getMonthWeekNumber(d) / self._domainType.week.row(d));
452
+ }
453
+ },
454
+ y: function(d) {
455
+ return self.getWeekNumber(d) % self._domainType.week.row(d);
456
+ }
457
+ },
458
+ format: {
459
+ date: "%B Week #%W",
460
+ legend: "%B Week #%W",
461
+ connector: "on"
462
+ },
463
+ extractUnit: function(d) {
464
+ var dt = new Date(d.getFullYear(), d.getMonth(), d.getDate());
465
+ // According to ISO-8601, week number computation are based on week starting on Monday
466
+ var weekDay = dt.getDay()-1;
467
+ if (weekDay < 0) {
468
+ weekDay = 6;
469
+ }
470
+ dt.setDate(dt.getDate() - weekDay);
471
+ return dt.getTime();
472
+ }
473
+ },
474
+ "month": {
475
+ name: "month",
476
+ level: 50,
477
+ maxItemNumber: 12,
478
+ defaultColumnNumber: 12,
479
+ defaultRowNumber: 1,
480
+ row: function() { return self.getSubDomainRowNumber(); },
481
+ column: function() { return self.getSubDomainColumnNumber(); },
482
+ position: {
483
+ x: function(d) { return Math.floor(d.getMonth() / self._domainType.month.row(d)); },
484
+ y: function(d) { return d.getMonth() % self._domainType.month.row(d); }
485
+ },
486
+ format: {
487
+ date: "%B %Y",
488
+ legend: "%B",
489
+ connector: "on"
490
+ },
491
+ extractUnit: function(d) {
492
+ return new Date(d.getFullYear(), d.getMonth()).getTime();
493
+ }
494
+ },
495
+ "year": {
496
+ name: "year",
497
+ level: 60,
498
+ row: function() { return self.options.rowLimit || 1; },
499
+ column: function() { return self.options.colLimit || 1; },
500
+ position: {
501
+ x: function() { return 1; },
502
+ y: function() { return 1; }
503
+ },
504
+ format: {
505
+ date: "%Y",
506
+ legend: "%Y",
507
+ connector: "on"
508
+ },
509
+ extractUnit: function(d) {
510
+ return new Date(d.getFullYear()).getTime();
511
+ }
512
+ }
513
+ };
514
+
515
+ for (var type in this._domainType) {
516
+ if (this._domainType.hasOwnProperty(type)) {
517
+ var d = this._domainType[type];
518
+ this._domainType["x_" + type] = {
519
+ name: "x_" + type,
520
+ level: d.type,
521
+ maxItemNumber: d.maxItemNumber,
522
+ defaultRowNumber: d.defaultRowNumber,
523
+ defaultColumnNumber: d.defaultColumnNumber,
524
+ row: d.column,
525
+ column: d.row,
526
+ position: {
527
+ x: d.position.y,
528
+ y: d.position.x,
529
+ },
530
+ format: d.format,
531
+ extractUnit: d.extractUnit
532
+ };
533
+ }
534
+ }
535
+
536
+ // Record the address of the last inserted domain when browsing
537
+ this.lastInsertedSvg = null;
538
+
539
+ this._completed = false;
540
+
541
+ // Record all the valid domains
542
+ // Each domain value is a timestamp in milliseconds
543
+ this._domains = d3.map();
544
+
545
+ this.graphDim = {
546
+ width: 0,
547
+ height: 0
548
+ };
549
+
550
+ this.legendDim = {
551
+ width: 0,
552
+ height: 0
553
+ };
554
+
555
+ this.NAVIGATE_LEFT = 1;
556
+ this.NAVIGATE_RIGHT = 2;
557
+
558
+ // Various update mode when using the update() API
559
+ this.RESET_ALL_ON_UPDATE = 0;
560
+ this.RESET_SINGLE_ON_UPDATE = 1;
561
+ this.APPEND_ON_UPDATE = 2;
562
+
563
+ this.DEFAULT_LEGEND_MARGIN = 10;
564
+
565
+ this.root = null;
566
+ this.tooltip = null;
567
+
568
+ this._maxDomainReached = false;
569
+ this._minDomainReached = false;
570
+
571
+ this.domainPosition = new DomainPosition();
572
+ this.Legend = null;
573
+ this.legendScale = null;
574
+
575
+ // List of domains that are skipped because of DST
576
+ // All times belonging to these domains should be re-assigned to the previous domain
577
+ this.DSTDomain = [];
578
+
579
+ /**
580
+ * Display the graph for the first time
581
+ * @return bool True if the calendar is created
582
+ */
583
+ this._init = function() {
584
+
585
+ self.getDomain(self.options.start).map(function(d) { return d.getTime(); }).map(function(d) {
586
+ self._domains.set(d, self.getSubDomain(d).map(function(d) { return {t: self._domainType[self.options.subDomain].extractUnit(d), v: null}; }));
587
+ });
588
+
589
+ self.root = d3.select(self.options.itemSelector).append("svg").attr("class", "cal-heatmap-container");
590
+
591
+ self.tooltip = d3.select(self.options.itemSelector)
592
+ .attr("style", function() {
593
+ var current = d3.select(self.options.itemSelector).attr("style");
594
+ return (current !== null ? current : "") + "position:relative;";
595
+ })
596
+ .append("div")
597
+ .attr("class", "ch-tooltip")
598
+ ;
599
+
600
+ self.root.attr("x", 0).attr("y", 0).append("svg").attr("class", "graph");
601
+
602
+ self.Legend = new Legend(self);
603
+
604
+ if (self.options.paintOnLoad) {
605
+ _initCalendar();
606
+ }
607
+
608
+ return true;
609
+ };
610
+
611
+ function _initCalendar() {
612
+ self.verticalDomainLabel = (self.options.label.position === "top" || self.options.label.position === "bottom");
613
+
614
+ self.domainVerticalLabelHeight = self.options.label.height === null ? Math.max(25, self.options.cellSize*2): self.options.label.height;
615
+ self.domainHorizontalLabelWidth = 0;
616
+
617
+ if (self.options.domainLabelFormat === "" && self.options.label.height === null) {
618
+ self.domainVerticalLabelHeight = 0;
619
+ }
620
+
621
+ if (!self.verticalDomainLabel) {
622
+ self.domainVerticalLabelHeight = 0;
623
+ self.domainHorizontalLabelWidth = self.options.label.width;
624
+ }
625
+
626
+ self.paint();
627
+
628
+ // =========================================================================//
629
+ // ATTACHING DOMAIN NAVIGATION EVENT //
630
+ // =========================================================================//
631
+ if (self.options.nextSelector !== false) {
632
+ d3.select(self.options.nextSelector).on("click." + self.options.itemNamespace, function() {
633
+ d3.event.preventDefault();
634
+ return self.loadNextDomain(1);
635
+ });
636
+ }
637
+
638
+ if (self.options.previousSelector !== false) {
639
+ d3.select(self.options.previousSelector).on("click." + self.options.itemNamespace, function() {
640
+ d3.event.preventDefault();
641
+ return self.loadPreviousDomain(1);
642
+ });
643
+ }
644
+
645
+ self.Legend.redraw(self.graphDim.width - self.options.domainGutter - self.options.cellPadding);
646
+ self.afterLoad();
647
+
648
+ var domains = self.getDomainKeys();
649
+
650
+ // Fill the graph with some datas
651
+ if (self.options.loadOnInit) {
652
+ self.getDatas(
653
+ self.options.data,
654
+ new Date(domains[0]),
655
+ self.getSubDomain(domains[domains.length-1]).pop(),
656
+ function() {
657
+ self.fill();
658
+ self.onComplete();
659
+ }
660
+ );
661
+ } else {
662
+ self.onComplete();
663
+ }
664
+
665
+ self.checkIfMinDomainIsReached(domains[0]);
666
+ self.checkIfMaxDomainIsReached(self.getNextDomain().getTime());
667
+ }
668
+
669
+ // Return the width of the domain block, without the domain gutter
670
+ // @param int d Domain start timestamp
671
+ function w(d, outer) {
672
+ var width = self.options.cellSize*self._domainType[self.options.subDomain].column(d) + self.options.cellPadding*self._domainType[self.options.subDomain].column(d);
673
+ if (arguments.length === 2 && outer === true) {
674
+ return width += self.domainHorizontalLabelWidth + self.options.domainGutter + self.options.domainMargin[1] + self.options.domainMargin[3];
675
+ }
676
+ return width;
677
+ }
678
+
679
+ // Return the height of the domain block, without the domain gutter
680
+ function h(d, outer) {
681
+ var height = self.options.cellSize*self._domainType[self.options.subDomain].row(d) + self.options.cellPadding*self._domainType[self.options.subDomain].row(d);
682
+ if (arguments.length === 2 && outer === true) {
683
+ height += self.options.domainGutter + self.domainVerticalLabelHeight + self.options.domainMargin[0] + self.options.domainMargin[2];
684
+ }
685
+ return height;
686
+ }
687
+
688
+ /**
689
+ *
690
+ *
691
+ * @param int navigationDir
692
+ */
693
+ this.paint = function(navigationDir) {
694
+
695
+ var options = self.options;
696
+
697
+ if (arguments.length === 0) {
698
+ navigationDir = false;
699
+ }
700
+
701
+ // Painting all the domains
702
+ var domainSvg = self.root.select(".graph")
703
+ .selectAll(".graph-domain")
704
+ .data(
705
+ function() {
706
+ var data = self.getDomainKeys();
707
+ return navigationDir === self.NAVIGATE_LEFT ? data.reverse(): data;
708
+ },
709
+ function(d) { return d; }
710
+ )
711
+ ;
712
+
713
+ var enteringDomainDim = 0;
714
+ var exitingDomainDim = 0;
715
+
716
+ // =========================================================================//
717
+ // PAINTING DOMAIN //
718
+ // =========================================================================//
719
+
720
+ var svg = domainSvg
721
+ .enter()
722
+ .append("svg")
723
+ .attr("width", function(d) {
724
+ return w(d, true);
725
+ })
726
+ .attr("height", function(d) {
727
+ return h(d, true);
728
+ })
729
+ .attr("x", function(d) {
730
+ if (options.verticalOrientation) {
731
+ self.graphDim.width = Math.max(self.graphDim.width, w(d, true));
732
+ return 0;
733
+ } else {
734
+ return getDomainPosition(d, self.graphDim, "width", w(d, true));
735
+ }
736
+ })
737
+ .attr("y", function(d) {
738
+ if (options.verticalOrientation) {
739
+ return getDomainPosition(d, self.graphDim, "height", h(d, true));
740
+ } else {
741
+ self.graphDim.height = Math.max(self.graphDim.height, h(d, true));
742
+ return 0;
743
+ }
744
+ })
745
+ .attr("class", function(d) {
746
+ var classname = "graph-domain";
747
+ var date = new Date(d);
748
+ switch(options.domain) {
749
+ case "hour":
750
+ classname += " h_" + date.getHours();
751
+ /* falls through */
752
+ case "day":
753
+ classname += " d_" + date.getDate() + " dy_" + date.getDay();
754
+ /* falls through */
755
+ case "week":
756
+ classname += " w_" + self.getWeekNumber(date);
757
+ /* falls through */
758
+ case "month":
759
+ classname += " m_" + (date.getMonth() + 1);
760
+ /* falls through */
761
+ case "year":
762
+ classname += " y_" + date.getFullYear();
763
+ }
764
+ return classname;
765
+ })
766
+ ;
767
+
768
+ self.lastInsertedSvg = svg;
769
+
770
+ function getDomainPosition(domainIndex, graphDim, axis, domainDim) {
771
+ var tmp = 0;
772
+ switch(navigationDir) {
773
+ case false:
774
+ tmp = graphDim[axis];
775
+
776
+ graphDim[axis] += domainDim;
777
+ self.domainPosition.setPosition(domainIndex, tmp);
778
+ return tmp;
779
+
780
+ case self.NAVIGATE_RIGHT:
781
+ self.domainPosition.setPosition(domainIndex, graphDim[axis]);
782
+
783
+ enteringDomainDim = domainDim;
784
+ exitingDomainDim = self.domainPosition.getPositionFromIndex(1);
785
+
786
+ self.domainPosition.shiftRightBy(exitingDomainDim);
787
+ return graphDim[axis];
788
+
789
+ case self.NAVIGATE_LEFT:
790
+ tmp = -domainDim;
791
+
792
+ enteringDomainDim = -tmp;
793
+ exitingDomainDim = graphDim[axis] - self.domainPosition.getLast();
794
+
795
+ self.domainPosition.setPosition(domainIndex, tmp);
796
+ self.domainPosition.shiftLeftBy(enteringDomainDim);
797
+ return tmp;
798
+ }
799
+ }
800
+
801
+ svg.append("rect")
802
+ .attr("width", function(d) { return w(d, true) - options.domainGutter - options.cellPadding; })
803
+ .attr("height", function(d) { return h(d, true) - options.domainGutter - options.cellPadding; })
804
+ .attr("class", "domain-background")
805
+ ;
806
+
807
+ // =========================================================================//
808
+ // PAINTING SUBDOMAINS //
809
+ // =========================================================================//
810
+ var subDomainSvgGroup = svg.append("svg")
811
+ .attr("x", function() {
812
+ if (options.label.position === "left") {
813
+ return self.domainHorizontalLabelWidth + options.domainMargin[3];
814
+ } else {
815
+ return options.domainMargin[3];
816
+ }
817
+ })
818
+ .attr("y", function() {
819
+ if (options.label.position === "top") {
820
+ return self.domainVerticalLabelHeight + options.domainMargin[0];
821
+ } else {
822
+ return options.domainMargin[0];
823
+ }
824
+ })
825
+ .attr("class", "graph-subdomain-group")
826
+ ;
827
+
828
+ var rect = subDomainSvgGroup
829
+ .selectAll("g")
830
+ .data(function(d) { return self._domains.get(d); })
831
+ .enter()
832
+ .append("g")
833
+ ;
834
+
835
+ rect
836
+ .append("rect")
837
+ .attr("class", function(d) {
838
+ return "graph-rect" + self.getHighlightClassName(d.t) + (options.onClick !== null ? " hover_cursor": "");
839
+ })
840
+ .attr("width", options.cellSize)
841
+ .attr("height", options.cellSize)
842
+ .attr("x", function(d) { return self.positionSubDomainX(d.t); })
843
+ .attr("y", function(d) { return self.positionSubDomainY(d.t); })
844
+ .on("click", function(d) {
845
+ if (options.onClick !== null) {
846
+ return self.onClick(new Date(d.t), d.v);
847
+ }
848
+ })
849
+ .call(function(selection) {
850
+ if (options.cellRadius > 0) {
851
+ selection
852
+ .attr("rx", options.cellRadius)
853
+ .attr("ry", options.cellRadius)
854
+ ;
855
+ }
856
+
857
+ if (self.legendScale !== null && options.legendColors !== null && options.legendColors.hasOwnProperty("base")) {
858
+ selection.attr("fill", options.legendColors.base);
859
+ }
860
+
861
+ if (options.tooltip) {
862
+ selection.on("mouseover", function(d) {
863
+ var domainNode = this.parentNode.parentNode.parentNode;
864
+
865
+ self.tooltip
866
+ .html(self.getSubDomainTitle(d))
867
+ .attr("style", "display: block;")
868
+ ;
869
+
870
+ self.tooltip.attr("style",
871
+ "display: block; " +
872
+ "left: " + (self.positionSubDomainX(d.t) - self.tooltip[0][0].offsetWidth/2 + options.cellSize/2 + parseInt(domainNode.getAttribute("x"), 10)) + "px; " +
873
+ "top: " + (self.positionSubDomainY(d.t) - self.tooltip[0][0].offsetHeight - options.cellSize/2 + parseInt(domainNode.getAttribute("y"), 10)) + "px;")
874
+ ;
875
+ });
876
+
877
+ selection.on("mouseout", function() {
878
+ self.tooltip
879
+ .attr("style", "display:none")
880
+ .html("");
881
+ });
882
+ }
883
+ })
884
+ ;
885
+
886
+ // Appending a title to each subdomain
887
+ if (!options.tooltip) {
888
+ rect.append("title").text(function(d){ return self.formatDate(new Date(d.t), options.subDomainDateFormat); });
889
+ }
890
+
891
+ // =========================================================================//
892
+ // PAINTING LABEL //
893
+ // =========================================================================//
894
+ if (options.domainLabelFormat !== "") {
895
+ svg.append("text")
896
+ .attr("class", "graph-label")
897
+ .attr("y", function(d) {
898
+ var y = options.domainMargin[0];
899
+ switch(options.label.position) {
900
+ case "top":
901
+ y += self.domainVerticalLabelHeight/2;
902
+ break;
903
+ case "bottom":
904
+ y += h(d) + self.domainVerticalLabelHeight/2;
905
+ }
906
+
907
+ return y + options.label.offset.y *
908
+ (
909
+ ((options.label.rotate === "right" && options.label.position === "right") ||
910
+ (options.label.rotate === "left" && options.label.position === "left")) ?
911
+ -1: 1
912
+ );
913
+ })
914
+ .attr("x", function(d){
915
+ var x = options.domainMargin[3];
916
+ switch(options.label.position) {
917
+ case "right":
918
+ x += w(d);
919
+ break;
920
+ case "bottom":
921
+ case "top":
922
+ x += w(d)/2;
923
+ }
924
+
925
+ if (options.label.align === "right") {
926
+ return x + self.domainHorizontalLabelWidth - options.label.offset.x *
927
+ (options.label.rotate === "right" ? -1: 1);
928
+ }
929
+ return x + options.label.offset.x;
930
+
931
+ })
932
+ .attr("text-anchor", function() {
933
+ switch(options.label.align) {
934
+ case "start":
935
+ case "left":
936
+ return "start";
937
+ case "end":
938
+ case "right":
939
+ return "end";
940
+ default:
941
+ return "middle";
942
+ }
943
+ })
944
+ .attr("dominant-baseline", function() { return self.verticalDomainLabel ? "middle": "top"; })
945
+ .text(function(d) { return self.formatDate(new Date(d), options.domainLabelFormat); })
946
+ .call(domainRotate)
947
+ ;
948
+ }
949
+
950
+ function domainRotate(selection) {
951
+ switch (options.label.rotate) {
952
+ case "right":
953
+ selection
954
+ .attr("transform", function(d) {
955
+ var s = "rotate(90), ";
956
+ switch(options.label.position) {
957
+ case "right":
958
+ s += "translate(-" + w(d) + " , -" + w(d) + ")";
959
+ break;
960
+ case "left":
961
+ s += "translate(0, -" + self.domainHorizontalLabelWidth + ")";
962
+ break;
963
+ }
964
+
965
+ return s;
966
+ });
967
+ break;
968
+ case "left":
969
+ selection
970
+ .attr("transform", function(d) {
971
+ var s = "rotate(270), ";
972
+ switch(options.label.position) {
973
+ case "right":
974
+ s += "translate(-" + (w(d) + self.domainHorizontalLabelWidth) + " , " + w(d) + ")";
975
+ break;
976
+ case "left":
977
+ s += "translate(-" + (self.domainHorizontalLabelWidth) + " , " + self.domainHorizontalLabelWidth + ")";
978
+ break;
979
+ }
980
+
981
+ return s;
982
+ });
983
+ break;
984
+ }
985
+ }
986
+
987
+ // =========================================================================//
988
+ // PAINTING DOMAIN SUBDOMAIN CONTENT //
989
+ // =========================================================================//
990
+ if (options.subDomainTextFormat !== null) {
991
+ rect
992
+ .append("text")
993
+ .attr("class", function(d) { return "subdomain-text" + self.getHighlightClassName(d.t); })
994
+ .attr("x", function(d) { return self.positionSubDomainX(d.t) + options.cellSize/2; })
995
+ .attr("y", function(d) { return self.positionSubDomainY(d.t) + options.cellSize/2; })
996
+ .attr("text-anchor", "middle")
997
+ .attr("dominant-baseline", "central")
998
+ .text(function(d){
999
+ return self.formatDate(new Date(d.t), options.subDomainTextFormat);
1000
+ })
1001
+ ;
1002
+ }
1003
+
1004
+ // =========================================================================//
1005
+ // ANIMATION //
1006
+ // =========================================================================//
1007
+
1008
+ if (navigationDir !== false) {
1009
+ domainSvg.transition().duration(options.animationDuration)
1010
+ .attr("x", function(d){
1011
+ return options.verticalOrientation ? 0: self.domainPosition.getPosition(d);
1012
+ })
1013
+ .attr("y", function(d){
1014
+ return options.verticalOrientation? self.domainPosition.getPosition(d): 0;
1015
+ })
1016
+ ;
1017
+ }
1018
+
1019
+ var tempWidth = self.graphDim.width;
1020
+ var tempHeight = self.graphDim.height;
1021
+
1022
+ if (options.verticalOrientation) {
1023
+ self.graphDim.height += enteringDomainDim - exitingDomainDim;
1024
+ } else {
1025
+ self.graphDim.width += enteringDomainDim - exitingDomainDim;
1026
+ }
1027
+
1028
+ // At the time of exit, domainsWidth and domainsHeight already automatically shifted
1029
+ domainSvg.exit().transition().duration(options.animationDuration)
1030
+ .attr("x", function(d){
1031
+ if (options.verticalOrientation) {
1032
+ return 0;
1033
+ } else {
1034
+ switch(navigationDir) {
1035
+ case self.NAVIGATE_LEFT:
1036
+ return Math.min(self.graphDim.width, tempWidth);
1037
+ case self.NAVIGATE_RIGHT:
1038
+ return -w(d, true);
1039
+ }
1040
+ }
1041
+ })
1042
+ .attr("y", function(d){
1043
+ if (options.verticalOrientation) {
1044
+ switch(navigationDir) {
1045
+ case self.NAVIGATE_LEFT:
1046
+ return Math.min(self.graphDim.height, tempHeight);
1047
+ case self.NAVIGATE_RIGHT:
1048
+ return -h(d, true);
1049
+ }
1050
+ } else {
1051
+ return 0;
1052
+ }
1053
+ })
1054
+ .remove()
1055
+ ;
1056
+
1057
+ // Resize the root container
1058
+ self.resize();
1059
+ };
1060
+ };
1061
+
1062
+ CalHeatMap.prototype = {
1063
+
1064
+ /**
1065
+ * Validate and merge user settings with default settings
1066
+ *
1067
+ * @param {object} settings User settings
1068
+ * @return {bool} False if settings contains error
1069
+ */
1070
+ /* jshint maxstatements:false */
1071
+ init: function(settings) {
1072
+ "use strict";
1073
+
1074
+ var parent = this;
1075
+
1076
+ var options = parent.options = mergeRecursive(parent.options, settings);
1077
+
1078
+ // Fatal errors
1079
+ // Stop script execution on error
1080
+ validateDomainType();
1081
+ validateSelector(options.itemSelector, false, "itemSelector");
1082
+
1083
+ if (parent.allowedDataType.indexOf(options.dataType) === -1) {
1084
+ throw new Error("The data type '" + options.dataType + "' is not valid data type");
1085
+ }
1086
+
1087
+ if (d3.select(options.itemSelector)[0][0] === null) {
1088
+ throw new Error("The node '" + options.itemSelector + "' specified in itemSelector does not exists");
1089
+ }
1090
+
1091
+ try {
1092
+ validateSelector(options.nextSelector, true, "nextSelector");
1093
+ validateSelector(options.previousSelector, true, "previousSelector");
1094
+ } catch(error) {
1095
+ console.log(error.message);
1096
+ return false;
1097
+ }
1098
+
1099
+ // If other settings contains error, will fallback to default
1100
+
1101
+ if (!settings.hasOwnProperty("subDomain")) {
1102
+ this.options.subDomain = getOptimalSubDomain(settings.domain);
1103
+ }
1104
+
1105
+ if (typeof options.itemNamespace !== "string" || options.itemNamespace === "") {
1106
+ console.log("itemNamespace can not be empty, falling back to cal-heatmap");
1107
+ options.itemNamespace = "cal-heatmap";
1108
+ }
1109
+
1110
+ // Don't touch these settings
1111
+ var s = ["data", "onComplete", "onClick", "afterLoad", "afterLoadData", "afterLoadPreviousDomain", "afterLoadNextDomain"];
1112
+
1113
+ for (var k in s) {
1114
+ if (settings.hasOwnProperty(s[k])) {
1115
+ options[s[k]] = settings[s[k]];
1116
+ }
1117
+ }
1118
+
1119
+ options.subDomainDateFormat = (typeof options.subDomainDateFormat === "string" || typeof options.subDomainDateFormat === "function" ? options.subDomainDateFormat : this._domainType[options.subDomain].format.date);
1120
+ options.domainLabelFormat = (typeof options.domainLabelFormat === "string" || typeof options.domainLabelFormat === "function" ? options.domainLabelFormat : this._domainType[options.domain].format.legend);
1121
+ options.subDomainTextFormat = ((typeof options.subDomainTextFormat === "string" && options.subDomainTextFormat !== "") || typeof options.subDomainTextFormat === "function" ? options.subDomainTextFormat : null);
1122
+ options.domainMargin = expandMarginSetting(options.domainMargin);
1123
+ options.legendMargin = expandMarginSetting(options.legendMargin);
1124
+ options.highlight = parent.expandDateSetting(options.highlight);
1125
+ options.itemName = expandItemName(options.itemName);
1126
+ options.colLimit = parseColLimit(options.colLimit);
1127
+ options.rowLimit = parseRowLimit(options.rowLimit);
1128
+ if (!settings.hasOwnProperty("legendMargin")) {
1129
+ autoAddLegendMargin();
1130
+ }
1131
+ autoAlignLabel();
1132
+
1133
+ /**
1134
+ * Validate that a queryString is valid
1135
+ *
1136
+ * @param {Element|string|bool} selector The queryString to test
1137
+ * @param {bool} canBeFalse Whether false is an accepted and valid value
1138
+ * @param {string} name Name of the tested selector
1139
+ * @throws {Error} If the selector is not valid
1140
+ * @return {bool} True if the selector is a valid queryString
1141
+ */
1142
+ function validateSelector(selector, canBeFalse, name) {
1143
+ if (((canBeFalse && selector === false) || selector instanceof Element || typeof selector === "string") && selector !== "") {
1144
+ return true;
1145
+ }
1146
+ throw new Error("The " + name + " is not valid");
1147
+ }
1148
+
1149
+ /**
1150
+ * Return the optimal subDomain for the specified domain
1151
+ *
1152
+ * @param {string} domain a domain name
1153
+ * @return {string} the subDomain name
1154
+ */
1155
+ function getOptimalSubDomain(domain) {
1156
+ switch(domain) {
1157
+ case "year":
1158
+ return "month";
1159
+ case "month":
1160
+ return "day";
1161
+ case "week":
1162
+ return "day";
1163
+ case "day":
1164
+ return "hour";
1165
+ default:
1166
+ return "min";
1167
+ }
1168
+ }
1169
+
1170
+ /**
1171
+ * Ensure that the domain and subdomain are valid
1172
+ *
1173
+ * @throw {Error} when domain or subdomain are not valid
1174
+ * @return {bool} True if domain and subdomain are valid and compatible
1175
+ */
1176
+ function validateDomainType() {
1177
+ if (!parent._domainType.hasOwnProperty(options.domain) || options.domain === "min" || options.domain.substring(0, 2) === "x_") {
1178
+ throw new Error("The domain '" + options.domain + "' is not valid");
1179
+ }
1180
+
1181
+ if (!parent._domainType.hasOwnProperty(options.subDomain) || options.subDomain === "year") {
1182
+ throw new Error("The subDomain '" + options.subDomain + "' is not valid");
1183
+ }
1184
+
1185
+ if (parent._domainType[options.domain].level <= parent._domainType[options.subDomain].level) {
1186
+ throw new Error("'" + options.subDomain + "' is not a valid subDomain to '" + options.domain + "'");
1187
+ }
1188
+
1189
+ return true;
1190
+ }
1191
+
1192
+ /**
1193
+ * Fine-tune the label alignement depending on its position
1194
+ *
1195
+ * @return void
1196
+ */
1197
+ function autoAlignLabel() {
1198
+ // Auto-align label, depending on it's position
1199
+ if (!settings.hasOwnProperty("label") || (settings.hasOwnProperty("label") && !settings.label.hasOwnProperty("align"))) {
1200
+ switch(options.label.position) {
1201
+ case "left":
1202
+ options.label.align = "right";
1203
+ break;
1204
+ case "right":
1205
+ options.label.align = "left";
1206
+ break;
1207
+ default:
1208
+ options.label.align = "center";
1209
+ }
1210
+
1211
+ if (options.label.rotate === "left") {
1212
+ options.label.align = "right";
1213
+ } else if (options.label.rotate === "right") {
1214
+ options.label.align = "left";
1215
+ }
1216
+ }
1217
+
1218
+ if (!settings.hasOwnProperty("label") || (settings.hasOwnProperty("label") && !settings.label.hasOwnProperty("offset"))) {
1219
+ if (options.label.position === "left" || options.label.position === "right") {
1220
+ options.label.offset = {
1221
+ x: 10,
1222
+ y: 15
1223
+ };
1224
+ }
1225
+ }
1226
+ }
1227
+
1228
+ /**
1229
+ * If not specified, add some margin around the legend depending on its position
1230
+ *
1231
+ * @return void
1232
+ */
1233
+ function autoAddLegendMargin() {
1234
+ switch(options.legendVerticalPosition) {
1235
+ case "top":
1236
+ options.legendMargin[2] = parent.DEFAULT_LEGEND_MARGIN;
1237
+ break;
1238
+ case "bottom":
1239
+ options.legendMargin[0] = parent.DEFAULT_LEGEND_MARGIN;
1240
+ break;
1241
+ case "middle":
1242
+ case "center":
1243
+ options.legendMargin[options.legendHorizontalPosition === "right" ? 3 : 1] = parent.DEFAULT_LEGEND_MARGIN;
1244
+ }
1245
+ }
1246
+
1247
+ /**
1248
+ * Expand a number of an array of numbers to an usable 4 values array
1249
+ *
1250
+ * @param {integer|array} value
1251
+ * @return {array} array
1252
+ */
1253
+ function expandMarginSetting(value) {
1254
+ if (typeof value === "number") {
1255
+ value = [value];
1256
+ }
1257
+
1258
+ if (!Array.isArray(value)) {
1259
+ console.log("Margin only takes an integer or an array of integers");
1260
+ value = [0];
1261
+ }
1262
+
1263
+ switch(value.length) {
1264
+ case 1:
1265
+ return [value[0], value[0], value[0], value[0]];
1266
+ case 2:
1267
+ return [value[0], value[1], value[0], value[1]];
1268
+ case 3:
1269
+ return [value[0], value[1], value[2], value[1]];
1270
+ case 4:
1271
+ return value;
1272
+ default:
1273
+ return value.slice(0, 4);
1274
+ }
1275
+ }
1276
+
1277
+ /**
1278
+ * Convert a string to an array like [singular-form, plural-form]
1279
+ *
1280
+ * @param {string|array} value Date to convert
1281
+ * @return {array} An array like [singular-form, plural-form]
1282
+ */
1283
+ function expandItemName(value) {
1284
+ if (typeof value === "string") {
1285
+ return [value, value + (value !== "" ? "s" : "")];
1286
+ }
1287
+
1288
+ if (Array.isArray(value)) {
1289
+ if (value.length === 1) {
1290
+ return [value[0], value[0] + "s"];
1291
+ } else if (value.length > 2) {
1292
+ return value.slice(0, 2);
1293
+ }
1294
+
1295
+ return value;
1296
+ }
1297
+
1298
+ return ["item", "items"];
1299
+ }
1300
+
1301
+ function parseColLimit(value) {
1302
+ return value > 0 ? value : null;
1303
+ }
1304
+
1305
+ function parseRowLimit(value) {
1306
+ if (value > 0 && options.colLimit > 0) {
1307
+ console.log("colLimit and rowLimit are mutually exclusive, rowLimit will be ignored");
1308
+ return null;
1309
+ }
1310
+ return value > 0 ? value : null;
1311
+ }
1312
+
1313
+ return this._init();
1314
+
1315
+ },
1316
+
1317
+ /**
1318
+ * Convert a keyword or an array of keyword/date to an array of date objects
1319
+ *
1320
+ * @param {string|array|Date} value Data to convert
1321
+ * @return {array} An array of Dates
1322
+ */
1323
+ expandDateSetting: function(value) {
1324
+ "use strict";
1325
+
1326
+ if (!Array.isArray(value)) {
1327
+ value = [value];
1328
+ }
1329
+
1330
+ return value.map(function(data) {
1331
+ if (data === "now") {
1332
+ return new Date();
1333
+ }
1334
+ if (data instanceof Date) {
1335
+ return data;
1336
+ }
1337
+ return false;
1338
+ }).filter(function(d) { return d !== false; });
1339
+ },
1340
+
1341
+ /**
1342
+ * Fill the calendar by coloring the cells
1343
+ *
1344
+ * @param array svg An array of html node to apply the transformation to (optional)
1345
+ * It's used to limit the painting to only a subset of the calendar
1346
+ * @return void
1347
+ */
1348
+ fill: function(svg) {
1349
+ "use strict";
1350
+
1351
+ var parent = this;
1352
+ var options = parent.options;
1353
+
1354
+ if (arguments.length === 0) {
1355
+ svg = parent.root.selectAll(".graph-domain");
1356
+ }
1357
+
1358
+ var rect = svg
1359
+ .selectAll("svg").selectAll("g")
1360
+ .data(function(d) { return parent._domains.get(d); })
1361
+ ;
1362
+
1363
+ /**
1364
+ * Colorize the cell via a style attribute if enabled
1365
+ */
1366
+ function addStyle(element) {
1367
+ if (parent.legendScale === null) {
1368
+ return false;
1369
+ }
1370
+
1371
+ element.attr("fill", function(d) {
1372
+ if (d.v === 0 && options.legendColors !== null && options.legendColors.hasOwnProperty("empty")) {
1373
+ return options.legendColors.empty;
1374
+ }
1375
+
1376
+ if (d.v < 0 && options.legend[0] > 0 && options.legendColors !== null && options.legendColors.hasOwnProperty("overflow")) {
1377
+ return options.legendColors.overflow;
1378
+ }
1379
+
1380
+ return parent.legendScale(Math.min(d.v, options.legend[options.legend.length-1]));
1381
+ });
1382
+ }
1383
+
1384
+ rect.transition().duration(options.animationDuration).select("rect")
1385
+ .attr("class", function(d) {
1386
+
1387
+ var htmlClass = parent.getHighlightClassName(d.t);
1388
+
1389
+ if (parent.legendScale === null) {
1390
+ htmlClass += " graph-rect";
1391
+ }
1392
+
1393
+ if (d.v !== null) {
1394
+ htmlClass += " " + parent.Legend.getClass(d.v, (parent.legendScale === null));
1395
+ } else if (options.considerMissingDataAsZero &&
1396
+ parent.dateIsLessThan(d.t, new Date())) {
1397
+ htmlClass += " " + parent.Legend.getClass(0, (parent.legendScale === null));
1398
+ }
1399
+
1400
+ if (options.onClick !== null) {
1401
+ htmlClass += " hover_cursor";
1402
+ }
1403
+
1404
+ return htmlClass;
1405
+ })
1406
+ .call(addStyle)
1407
+ ;
1408
+
1409
+ rect.transition().duration(options.animationDuration).select("title")
1410
+ .text(function(d) { return parent.getSubDomainTitle(d); })
1411
+ ;
1412
+
1413
+ function formatSubDomainText(element) {
1414
+ if (typeof options.subDomainTextFormat === "function") {
1415
+ element.text(function(d) { return options.subDomainTextFormat(d.t, d.v); });
1416
+ }
1417
+ }
1418
+
1419
+ /**
1420
+ * Change the subDomainText class if necessary
1421
+ * Also change the text, e.g when text is representing the value
1422
+ * instead of the date
1423
+ */
1424
+ rect.transition().duration(options.animationDuration).select("text")
1425
+ .attr("class", function(d) { return "subdomain-text" + parent.getHighlightClassName(d.t); })
1426
+ .call(formatSubDomainText)
1427
+ ;
1428
+ },
1429
+
1430
+ // =========================================================================//
1431
+ // EVENTS CALLBACK //
1432
+ // =========================================================================//
1433
+
1434
+ /**
1435
+ * Helper method for triggering event callback
1436
+ *
1437
+ * @param string eventName Name of the event to trigger
1438
+ * @param array successArgs List of argument to pass to the callback
1439
+ * @param boolean skip Whether to skip the event triggering
1440
+ * @return mixed True when the triggering was skipped, false on error, else the callback function
1441
+ */
1442
+ triggerEvent: function(eventName, successArgs, skip) {
1443
+ "use strict";
1444
+
1445
+ if ((arguments.length === 3 && skip) || this.options[eventName] === null) {
1446
+ return true;
1447
+ }
1448
+
1449
+ if (typeof this.options[eventName] === "function") {
1450
+ if (typeof successArgs === "function") {
1451
+ successArgs = successArgs();
1452
+ }
1453
+ return this.options[eventName].apply(this, successArgs);
1454
+ } else {
1455
+ console.log("Provided callback for " + eventName + " is not a function.");
1456
+ return false;
1457
+ }
1458
+ },
1459
+
1460
+ /**
1461
+ * Event triggered on a mouse click on a subDomain cell
1462
+ *
1463
+ * @param Date d Date of the subdomain block
1464
+ * @param int itemNb Number of items in that date
1465
+ */
1466
+ onClick: function(d, itemNb) {
1467
+ "use strict";
1468
+
1469
+ return this.triggerEvent("onClick", [d, itemNb]);
1470
+ },
1471
+
1472
+ /**
1473
+ * Event triggered after drawing the calendar, byt before filling it with data
1474
+ */
1475
+ afterLoad: function() {
1476
+ "use strict";
1477
+
1478
+ return this.triggerEvent("afterLoad");
1479
+ },
1480
+
1481
+ /**
1482
+ * Event triggered after completing drawing and filling the calendar
1483
+ */
1484
+ onComplete: function() {
1485
+ "use strict";
1486
+
1487
+ var response = this.triggerEvent("onComplete", [], this._completed);
1488
+ this._completed = true;
1489
+ return response;
1490
+ },
1491
+
1492
+ /**
1493
+ * Event triggered after shifting the calendar one domain back
1494
+ *
1495
+ * @param Date start Domain start date
1496
+ * @param Date end Domain end date
1497
+ */
1498
+ afterLoadPreviousDomain: function(start) {
1499
+ "use strict";
1500
+
1501
+ var parent = this;
1502
+ return this.triggerEvent("afterLoadPreviousDomain", function() {
1503
+ var subDomain = parent.getSubDomain(start);
1504
+ return [subDomain.shift(), subDomain.pop()];
1505
+ });
1506
+ },
1507
+
1508
+ /**
1509
+ * Event triggered after shifting the calendar one domain above
1510
+ *
1511
+ * @param Date start Domain start date
1512
+ * @param Date end Domain end date
1513
+ */
1514
+ afterLoadNextDomain: function(start) {
1515
+ "use strict";
1516
+
1517
+ var parent = this;
1518
+ return this.triggerEvent("afterLoadNextDomain", function() {
1519
+ var subDomain = parent.getSubDomain(start);
1520
+ return [subDomain.shift(), subDomain.pop()];
1521
+ });
1522
+ },
1523
+
1524
+ /**
1525
+ * Event triggered after loading the leftmost domain allowed by minDate
1526
+ *
1527
+ * @param boolean reached True if the leftmost domain was reached
1528
+ */
1529
+ onMinDomainReached: function(reached) {
1530
+ "use strict";
1531
+
1532
+ this._minDomainReached = reached;
1533
+ return this.triggerEvent("onMinDomainReached", [reached]);
1534
+ },
1535
+
1536
+ /**
1537
+ * Event triggered after loading the rightmost domain allowed by maxDate
1538
+ *
1539
+ * @param boolean reached True if the rightmost domain was reached
1540
+ */
1541
+ onMaxDomainReached: function(reached) {
1542
+ "use strict";
1543
+
1544
+ this._maxDomainReached = reached;
1545
+ return this.triggerEvent("onMaxDomainReached", [reached]);
1546
+ },
1547
+
1548
+ checkIfMinDomainIsReached: function(date, upperBound) {
1549
+ "use strict";
1550
+
1551
+ if (this.minDomainIsReached(date)) {
1552
+ this.onMinDomainReached(true);
1553
+ }
1554
+
1555
+ if (arguments.length === 2) {
1556
+ if (this._maxDomainReached && !this.maxDomainIsReached(upperBound)) {
1557
+ this.onMaxDomainReached(false);
1558
+ }
1559
+ }
1560
+ },
1561
+
1562
+ checkIfMaxDomainIsReached: function(date, lowerBound) {
1563
+ "use strict";
1564
+
1565
+ if (this.maxDomainIsReached(date)) {
1566
+ this.onMaxDomainReached(true);
1567
+ }
1568
+
1569
+ if (arguments.length === 2) {
1570
+ if (this._minDomainReached && !this.minDomainIsReached(lowerBound)) {
1571
+ this.onMinDomainReached(false);
1572
+ }
1573
+ }
1574
+ },
1575
+
1576
+ // =========================================================================//
1577
+ // FORMATTER //
1578
+ // =========================================================================//
1579
+
1580
+ formatNumber: d3.format(",g"),
1581
+
1582
+ formatDate: function(d, format) {
1583
+ "use strict";
1584
+
1585
+ if (arguments.length < 2) {
1586
+ format = "title";
1587
+ }
1588
+
1589
+ if (typeof format === "function") {
1590
+ return format(d);
1591
+ } else {
1592
+ var f = d3.time.format(format);
1593
+ return f(d);
1594
+ }
1595
+ },
1596
+
1597
+ getSubDomainTitle: function(d) {
1598
+ "use strict";
1599
+
1600
+ if (d.v === null && !this.options.considerMissingDataAsZero) {
1601
+ return (this.options.subDomainTitleFormat.empty).format({
1602
+ date: this.formatDate(new Date(d.t), this.options.subDomainDateFormat)
1603
+ });
1604
+ } else {
1605
+ var value = d.v;
1606
+ // Consider null as 0
1607
+ if (value === null && this.options.considerMissingDataAsZero) {
1608
+ value = 0;
1609
+ }
1610
+
1611
+ return (this.options.subDomainTitleFormat.filled).format({
1612
+ count: this.formatNumber(value),
1613
+ name: this.options.itemName[(value !== 1 ? 1: 0)],
1614
+ connector: this._domainType[this.options.subDomain].format.connector,
1615
+ date: this.formatDate(new Date(d.t), this.options.subDomainDateFormat)
1616
+ });
1617
+ }
1618
+ },
1619
+
1620
+ // =========================================================================//
1621
+ // DOMAIN NAVIGATION //
1622
+ // =========================================================================//
1623
+
1624
+ /**
1625
+ * Shift the calendar one domain forward
1626
+ *
1627
+ * The new domain is loaded only if it's not beyond maxDate
1628
+ *
1629
+ * @param int n Number of domains to load
1630
+ * @return bool True if the next domain was loaded, else false
1631
+ */
1632
+ loadNextDomain: function(n) {
1633
+ "use strict";
1634
+
1635
+ if (this._maxDomainReached || n === 0) {
1636
+ return false;
1637
+ }
1638
+
1639
+ var bound = this.loadNewDomains(this.NAVIGATE_RIGHT, this.getDomain(this.getNextDomain(), n));
1640
+
1641
+ this.afterLoadNextDomain(bound.end);
1642
+ this.checkIfMaxDomainIsReached(this.getNextDomain().getTime(), bound.start);
1643
+
1644
+ return true;
1645
+ },
1646
+
1647
+ /**
1648
+ * Shift the calendar one domain backward
1649
+ *
1650
+ * The previous domain is loaded only if it's not beyond the minDate
1651
+ *
1652
+ * @param int n Number of domains to load
1653
+ * @return bool True if the previous domain was loaded, else false
1654
+ */
1655
+ loadPreviousDomain: function(n) {
1656
+ "use strict";
1657
+
1658
+ if (this._minDomainReached || n === 0) {
1659
+ return false;
1660
+ }
1661
+
1662
+ var bound = this.loadNewDomains(this.NAVIGATE_LEFT, this.getDomain(this.getDomainKeys()[0], -n).reverse());
1663
+
1664
+ this.afterLoadPreviousDomain(bound.start);
1665
+ this.checkIfMinDomainIsReached(bound.start, bound.end);
1666
+
1667
+ return true;
1668
+ },
1669
+
1670
+ loadNewDomains: function(direction, newDomains) {
1671
+ "use strict";
1672
+
1673
+ var parent = this;
1674
+ var backward = direction === this.NAVIGATE_LEFT;
1675
+ var i = -1;
1676
+ var total = newDomains.length;
1677
+ var domains = this.getDomainKeys();
1678
+
1679
+ function buildSubDomain(d) {
1680
+ return {t: parent._domainType[parent.options.subDomain].extractUnit(d), v: null};
1681
+ }
1682
+
1683
+ // Remove out of bound domains from list of new domains to prepend
1684
+ while (++i < total) {
1685
+ if (backward && this.minDomainIsReached(newDomains[i])) {
1686
+ newDomains = newDomains.slice(0, i+1);
1687
+ break;
1688
+ }
1689
+ if (!backward && this.maxDomainIsReached(newDomains[i])) {
1690
+ newDomains = newDomains.slice(0, i);
1691
+ break;
1692
+ }
1693
+ }
1694
+
1695
+ newDomains = newDomains.slice(-this.options.range);
1696
+
1697
+ for (i = 0, total = newDomains.length; i < total; i++) {
1698
+ this._domains.set(
1699
+ newDomains[i].getTime(),
1700
+ this.getSubDomain(newDomains[i]).map(buildSubDomain)
1701
+ );
1702
+
1703
+ this._domains.remove(backward ? domains.pop() : domains.shift());
1704
+ }
1705
+
1706
+ domains = this.getDomainKeys();
1707
+
1708
+ if (backward) {
1709
+ newDomains = newDomains.reverse();
1710
+ }
1711
+
1712
+ this.paint(direction);
1713
+
1714
+ this.getDatas(
1715
+ this.options.data,
1716
+ newDomains[0],
1717
+ this.getSubDomain(newDomains[newDomains.length-1]).pop(),
1718
+ function() {
1719
+ parent.fill(parent.lastInsertedSvg);
1720
+ }
1721
+ );
1722
+
1723
+ return {
1724
+ start: newDomains[backward ? 0 : 1],
1725
+ end: domains[domains.length-1]
1726
+ };
1727
+ },
1728
+
1729
+ /**
1730
+ * Return whether a date is inside the scope determined by maxDate
1731
+ *
1732
+ * @param int datetimestamp The timestamp in ms to test
1733
+ * @return bool True if the specified date correspond to the calendar upper bound
1734
+ */
1735
+ maxDomainIsReached: function(datetimestamp) {
1736
+ "use strict";
1737
+
1738
+ return (this.options.maxDate !== null && (this.options.maxDate.getTime() < datetimestamp));
1739
+ },
1740
+
1741
+ /**
1742
+ * Return whether a date is inside the scope determined by minDate
1743
+ *
1744
+ * @param int datetimestamp The timestamp in ms to test
1745
+ * @return bool True if the specified date correspond to the calendar lower bound
1746
+ */
1747
+ minDomainIsReached: function (datetimestamp) {
1748
+ "use strict";
1749
+
1750
+ return (this.options.minDate !== null && (this.options.minDate.getTime() >= datetimestamp));
1751
+ },
1752
+
1753
+ /**
1754
+ * Return the list of the calendar's domain timestamp
1755
+ *
1756
+ * @return Array a sorted array of timestamp
1757
+ */
1758
+ getDomainKeys: function() {
1759
+ "use strict";
1760
+
1761
+ return this._domains.keys()
1762
+ .map(function(d) { return parseInt(d, 10); })
1763
+ .sort(function(a,b) { return a-b; });
1764
+ },
1765
+
1766
+ // =========================================================================//
1767
+ // POSITIONNING //
1768
+ // =========================================================================//
1769
+
1770
+ positionSubDomainX: function(d) {
1771
+ "use strict";
1772
+
1773
+ var index = this._domainType[this.options.subDomain].position.x(new Date(d));
1774
+ return index * this.options.cellSize + index * this.options.cellPadding;
1775
+ },
1776
+
1777
+ positionSubDomainY: function(d) {
1778
+ "use strict";
1779
+
1780
+ var index = this._domainType[this.options.subDomain].position.y(new Date(d));
1781
+ return index * this.options.cellSize + index * this.options.cellPadding;
1782
+ },
1783
+
1784
+ getSubDomainColumnNumber: function(d) {
1785
+ "use strict";
1786
+
1787
+ if (this.options.rowLimit > 0) {
1788
+ var i = this._domainType[this.options.subDomain].maxItemNumber;
1789
+ if (typeof i === "function") {
1790
+ i = i(d);
1791
+ }
1792
+ return Math.ceil(i / this.options.rowLimit);
1793
+ }
1794
+
1795
+ var j = this._domainType[this.options.subDomain].defaultColumnNumber;
1796
+ if (typeof j === "function") {
1797
+ j = j(d);
1798
+
1799
+ }
1800
+ return this.options.colLimit || j;
1801
+ },
1802
+
1803
+ getSubDomainRowNumber: function(d) {
1804
+ "use strict";
1805
+
1806
+ if (this.options.colLimit > 0) {
1807
+ var i = this._domainType[this.options.subDomain].maxItemNumber;
1808
+ if (typeof i === "function") {
1809
+ i = i(d);
1810
+ }
1811
+ return Math.ceil(i / this.options.colLimit);
1812
+ }
1813
+
1814
+ var j = this._domainType[this.options.subDomain].defaultRowNumber;
1815
+ if (typeof j === "function") {
1816
+ j = j(d);
1817
+
1818
+ }
1819
+ return this.options.rowLimit || j;
1820
+ },
1821
+
1822
+ /**
1823
+ * Return a classname if the specified date should be highlighted
1824
+ *
1825
+ * @param timestamp date Date of the current subDomain
1826
+ * @return String the highlight class
1827
+ */
1828
+ getHighlightClassName: function(d) {
1829
+ "use strict";
1830
+
1831
+ d = new Date(d);
1832
+
1833
+ if (this.options.highlight.length > 0) {
1834
+ for (var i in this.options.highlight) {
1835
+ if (this.options.highlight[i] instanceof Date && this.dateIsEqual(this.options.highlight[i], d)) {
1836
+ return " highlight" + (this.isNow(this.options.highlight[i]) ? " now": "");
1837
+ }
1838
+ }
1839
+ }
1840
+ return "";
1841
+ },
1842
+
1843
+ /**
1844
+ * Return whether the specified date is now,
1845
+ * according to the type of subdomain
1846
+ *
1847
+ * @param Date d The date to compare
1848
+ * @return bool True if the date correspond to a subdomain cell
1849
+ */
1850
+ isNow: function(d) {
1851
+ "use strict";
1852
+
1853
+ return this.dateIsEqual(d, new Date());
1854
+ },
1855
+
1856
+ /**
1857
+ * Return whether 2 dates are equals
1858
+ * This function is subdomain-aware,
1859
+ * and dates comparison are dependent of the subdomain
1860
+ *
1861
+ * @param Date dateA First date to compare
1862
+ * @param Date dateB Secon date to compare
1863
+ * @return bool true if the 2 dates are equals
1864
+ */
1865
+ /* jshint maxcomplexity: false */
1866
+ dateIsEqual: function(dateA, dateB) {
1867
+ "use strict";
1868
+
1869
+ switch(this.options.subDomain) {
1870
+ case "x_min":
1871
+ case "min":
1872
+ return dateA.getFullYear() === dateB.getFullYear() &&
1873
+ dateA.getMonth() === dateB.getMonth() &&
1874
+ dateA.getDate() === dateB.getDate() &&
1875
+ dateA.getHours() === dateB.getHours() &&
1876
+ dateA.getMinutes() === dateB.getMinutes();
1877
+ case "x_hour":
1878
+ case "hour":
1879
+ return dateA.getFullYear() === dateB.getFullYear() &&
1880
+ dateA.getMonth() === dateB.getMonth() &&
1881
+ dateA.getDate() === dateB.getDate() &&
1882
+ dateA.getHours() === dateB.getHours();
1883
+ case "x_day":
1884
+ case "day":
1885
+ return dateA.getFullYear() === dateB.getFullYear() &&
1886
+ dateA.getMonth() === dateB.getMonth() &&
1887
+ dateA.getDate() === dateB.getDate();
1888
+ case "x_week":
1889
+ case "week":
1890
+ case "x_month":
1891
+ case "month":
1892
+ return dateA.getFullYear() === dateB.getFullYear() &&
1893
+ dateA.getMonth() === dateB.getMonth();
1894
+ default:
1895
+ return false;
1896
+ }
1897
+ },
1898
+
1899
+
1900
+ /**
1901
+ * Returns weather or not dateA is less than or equal to dateB. This function is subdomain aware.
1902
+ * Performs automatic conversion of values.
1903
+ * @param dateA may be a number or a Date
1904
+ * @param dateB may be a number or a Date
1905
+ * @returns {boolean}
1906
+ */
1907
+ dateIsLessThan: function(dateA, dateB) {
1908
+ "use strict";
1909
+
1910
+ if(!(dateA instanceof Date)) {
1911
+ dateA = new Date(dateA);
1912
+ }
1913
+
1914
+ if (!(dateB instanceof Date)) {
1915
+ dateB = new Date(dateB);
1916
+ }
1917
+
1918
+
1919
+ function normalizedMillis(date, subdomain) {
1920
+ switch(subdomain) {
1921
+ case "x_min":
1922
+ case "min":
1923
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes()).getTime();
1924
+ case "x_hour":
1925
+ case "hour":
1926
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours()).getTime();
1927
+ case "x_day":
1928
+ case "day":
1929
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
1930
+ case "x_week":
1931
+ case "week":
1932
+ case "x_month":
1933
+ case "month":
1934
+ return new Date(date.getFullYear(), date.getMonth()).getTime();
1935
+ default:
1936
+ return date.getTime();
1937
+ }
1938
+ }
1939
+
1940
+ return normalizedMillis(dateA, this.options.subDomain) < normalizedMillis(dateB, this.options.subDomain);
1941
+ },
1942
+
1943
+
1944
+ // =========================================================================//
1945
+ // DATE COMPUTATION //
1946
+ // =========================================================================//
1947
+
1948
+ /**
1949
+ * Return the day of the year for the date
1950
+ * @param Date
1951
+ * @return int Day of the year [1,366]
1952
+ */
1953
+ getDayOfYear: d3.time.format("%j"),
1954
+
1955
+ /**
1956
+ * Return the week number of the year
1957
+ * Monday as the first day of the week
1958
+ * @return int Week number [0-53]
1959
+ */
1960
+ getWeekNumber: function(d) {
1961
+ "use strict";
1962
+
1963
+ var f = this.options.weekStartOnMonday === true ? d3.time.format("%W"): d3.time.format("%U");
1964
+ return f(d);
1965
+ },
1966
+
1967
+ /**
1968
+ * Return the week number, relative to its month
1969
+ *
1970
+ * @param int|Date d Date or timestamp in milliseconds
1971
+ * @return int Week number, relative to the month [0-5]
1972
+ */
1973
+ getMonthWeekNumber: function (d) {
1974
+ "use strict";
1975
+
1976
+ if (typeof d === "number") {
1977
+ d = new Date(d);
1978
+ }
1979
+
1980
+ var monthFirstWeekNumber = this.getWeekNumber(new Date(d.getFullYear(), d.getMonth()));
1981
+ return this.getWeekNumber(d) - monthFirstWeekNumber - 1;
1982
+ },
1983
+
1984
+ /**
1985
+ * Return the number of weeks in the dates' year
1986
+ *
1987
+ * @param int|Date d Date or timestamp in milliseconds
1988
+ * @return int Number of weeks in the date's year
1989
+ */
1990
+ getWeekNumberInYear: function(d) {
1991
+ "use strict";
1992
+
1993
+ if (typeof d === "number") {
1994
+ d = new Date(d);
1995
+ }
1996
+ },
1997
+
1998
+ /**
1999
+ * Return the number of days in the date's month
2000
+ *
2001
+ * @param int|Date d Date or timestamp in milliseconds
2002
+ * @return int Number of days in the date's month
2003
+ */
2004
+ getDayCountInMonth: function(d) {
2005
+ "use strict";
2006
+
2007
+ return this.getEndOfMonth(d).getDate();
2008
+ },
2009
+
2010
+ /**
2011
+ * Return the number of days in the date's year
2012
+ *
2013
+ * @param int|Date d Date or timestamp in milliseconds
2014
+ * @return int Number of days in the date's year
2015
+ */
2016
+ getDayCountInYear: function(d) {
2017
+ "use strict";
2018
+
2019
+ if (typeof d === "number") {
2020
+ d = new Date(d);
2021
+ }
2022
+ return (new Date(d.getFullYear(), 1, 29).getMonth() === 1) ? 366 : 365;
2023
+ },
2024
+
2025
+ /**
2026
+ * Get the weekday from a date
2027
+ *
2028
+ * Return the week day number (0-6) of a date,
2029
+ * depending on whether the week start on monday or sunday
2030
+ *
2031
+ * @param Date d
2032
+ * @return int The week day number (0-6)
2033
+ */
2034
+ getWeekDay: function(d) {
2035
+ "use strict";
2036
+
2037
+ if (this.options.weekStartOnMonday === false) {
2038
+ return d.getDay();
2039
+ }
2040
+ return d.getDay() === 0 ? 6 : (d.getDay()-1);
2041
+ },
2042
+
2043
+ /**
2044
+ * Get the last day of the month
2045
+ * @param Date|int d Date or timestamp in milliseconds
2046
+ * @return Date Last day of the month
2047
+ */
2048
+ getEndOfMonth: function(d) {
2049
+ "use strict";
2050
+
2051
+ if (typeof d === "number") {
2052
+ d = new Date(d);
2053
+ }
2054
+ return new Date(d.getFullYear(), d.getMonth()+1, 0);
2055
+ },
2056
+
2057
+ /**
2058
+ *
2059
+ * @param Date date
2060
+ * @param int count
2061
+ * @param string step
2062
+ * @return Date
2063
+ */
2064
+ jumpDate: function(date, count, step) {
2065
+ "use strict";
2066
+
2067
+ var d = new Date(date);
2068
+ switch(step) {
2069
+ case "hour":
2070
+ d.setHours(d.getHours() + count);
2071
+ break;
2072
+ case "day":
2073
+ d.setHours(d.getHours() + count * 24);
2074
+ break;
2075
+ case "week":
2076
+ d.setHours(d.getHours() + count * 24 * 7);
2077
+ break;
2078
+ case "month":
2079
+ d.setMonth(d.getMonth() + count);
2080
+ break;
2081
+ case "year":
2082
+ d.setFullYear(d.getFullYear() + count);
2083
+ }
2084
+
2085
+ return new Date(d);
2086
+ },
2087
+
2088
+ // =========================================================================//
2089
+ // DOMAIN COMPUTATION //
2090
+ // =========================================================================//
2091
+
2092
+ /**
2093
+ * Return all the minutes between 2 dates
2094
+ *
2095
+ * @param Date d date A date
2096
+ * @param int|date range Number of minutes in the range, or a stop date
2097
+ * @return array An array of minutes
2098
+ */
2099
+ getMinuteDomain: function (d, range) {
2100
+ "use strict";
2101
+
2102
+ var start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours());
2103
+ var stop = null;
2104
+ if (range instanceof Date) {
2105
+ stop = new Date(range.getFullYear(), range.getMonth(), range.getDate(), range.getHours());
2106
+ } else {
2107
+ stop = new Date(+start + range * 1000 * 60);
2108
+ }
2109
+ return d3.time.minutes(Math.min(start, stop), Math.max(start, stop));
2110
+ },
2111
+
2112
+ /**
2113
+ * Return all the hours between 2 dates
2114
+ *
2115
+ * @param Date d A date
2116
+ * @param int|date range Number of hours in the range, or a stop date
2117
+ * @return array An array of hours
2118
+ */
2119
+ getHourDomain: function (d, range) {
2120
+ "use strict";
2121
+
2122
+ var start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours());
2123
+ var stop = null;
2124
+ if (range instanceof Date) {
2125
+ stop = new Date(range.getFullYear(), range.getMonth(), range.getDate(), range.getHours());
2126
+ } else {
2127
+ stop = new Date(start);
2128
+ stop.setHours(stop.getHours() + range);
2129
+ }
2130
+
2131
+ var domains = d3.time.hours(Math.min(start, stop), Math.max(start, stop));
2132
+
2133
+ // Passing from DST to standard time
2134
+ // If there are 25 hours, let's compress the duplicate hours
2135
+ var i = 0;
2136
+ var total = domains.length;
2137
+ for(i = 0; i < total; i++) {
2138
+ if (i > 0 && (domains[i].getHours() === domains[i-1].getHours())) {
2139
+ this.DSTDomain.push(domains[i].getTime());
2140
+ domains.splice(i, 1);
2141
+ break;
2142
+ }
2143
+ }
2144
+
2145
+ // d3.time.hours is returning more hours than needed when changing
2146
+ // from DST to standard time, because there is really 2 hours between
2147
+ // 1am and 2am!
2148
+ if (typeof range === "number" && domains.length > Math.abs(range)) {
2149
+ domains.splice(domains.length-1, 1);
2150
+ }
2151
+
2152
+ return domains;
2153
+ },
2154
+
2155
+ /**
2156
+ * Return all the days between 2 dates
2157
+ *
2158
+ * @param Date d A date
2159
+ * @param int|date range Number of days in the range, or a stop date
2160
+ * @return array An array of weeks
2161
+ */
2162
+ getDayDomain: function (d, range) {
2163
+ "use strict";
2164
+
2165
+ var start = new Date(d.getFullYear(), d.getMonth(), d.getDate());
2166
+ var stop = null;
2167
+ if (range instanceof Date) {
2168
+ stop = new Date(range.getFullYear(), range.getMonth(), range.getDate());
2169
+ } else {
2170
+ stop = new Date(start);
2171
+ stop = new Date(stop.setDate(stop.getDate() + parseInt(range, 10)));
2172
+ }
2173
+
2174
+ return d3.time.days(Math.min(start, stop), Math.max(start, stop));
2175
+ },
2176
+
2177
+ /**
2178
+ * Return all the weeks between 2 dates
2179
+ *
2180
+ * @param Date d A date
2181
+ * @param int|date range Number of minutes in the range, or a stop date
2182
+ * @return array An array of weeks
2183
+ */
2184
+ getWeekDomain: function (d, range) {
2185
+ "use strict";
2186
+
2187
+ var weekStart;
2188
+
2189
+ if (this.options.weekStartOnMonday === false) {
2190
+ weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate() - d.getDay());
2191
+ } else {
2192
+ if (d.getDay() === 1) {
2193
+ weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate());
2194
+ } else if (d.getDay() === 0) {
2195
+ weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate());
2196
+ weekStart.setDate(weekStart.getDate() - 6);
2197
+ } else {
2198
+ weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()-d.getDay()+1);
2199
+ }
2200
+ }
2201
+
2202
+ var endDate = new Date(weekStart);
2203
+
2204
+ var stop = range;
2205
+ if (typeof range !== "object") {
2206
+ stop = new Date(endDate.setDate(endDate.getDate() + range * 7));
2207
+ }
2208
+
2209
+ return (this.options.weekStartOnMonday === true) ?
2210
+ d3.time.mondays(Math.min(weekStart, stop), Math.max(weekStart, stop)):
2211
+ d3.time.sundays(Math.min(weekStart, stop), Math.max(weekStart, stop))
2212
+ ;
2213
+ },
2214
+
2215
+ /**
2216
+ * Return all the months between 2 dates
2217
+ *
2218
+ * @param Date d A date
2219
+ * @param int|date range Number of months in the range, or a stop date
2220
+ * @return array An array of months
2221
+ */
2222
+ getMonthDomain: function (d, range) {
2223
+ "use strict";
2224
+
2225
+ var start = new Date(d.getFullYear(), d.getMonth());
2226
+ var stop = null;
2227
+ if (range instanceof Date) {
2228
+ stop = new Date(range.getFullYear(), range.getMonth());
2229
+ } else {
2230
+ stop = new Date(start);
2231
+ stop = stop.setMonth(stop.getMonth()+range);
2232
+ }
2233
+
2234
+ return d3.time.months(Math.min(start, stop), Math.max(start, stop));
2235
+ },
2236
+
2237
+ /**
2238
+ * Return all the years between 2 dates
2239
+ *
2240
+ * @param Date d date A date
2241
+ * @param int|date range Number of minutes in the range, or a stop date
2242
+ * @return array An array of hours
2243
+ */
2244
+ getYearDomain: function(d, range){
2245
+ "use strict";
2246
+
2247
+ var start = new Date(d.getFullYear(), 0);
2248
+ var stop = null;
2249
+ if (range instanceof Date) {
2250
+ stop = new Date(range.getFullYear(), 0);
2251
+ } else {
2252
+ stop = new Date(d.getFullYear()+range, 0);
2253
+ }
2254
+
2255
+ return d3.time.years(Math.min(start, stop), Math.max(start, stop));
2256
+ },
2257
+
2258
+ /**
2259
+ * Get an array of domain start dates
2260
+ *
2261
+ * @param int|Date date A random date included in the wanted domain
2262
+ * @param int|Date range Number of dates to get, or a stop date
2263
+ * @return Array of dates
2264
+ */
2265
+ getDomain: function(date, range) {
2266
+ "use strict";
2267
+
2268
+ if (typeof date === "number") {
2269
+ date = new Date(date);
2270
+ }
2271
+
2272
+ if (arguments.length < 2) {
2273
+ range = this.options.range;
2274
+ }
2275
+
2276
+ switch(this.options.domain) {
2277
+ case "hour" :
2278
+ var domains = this.getHourDomain(date, range);
2279
+
2280
+ // Case where an hour is missing, when passing from standard time to DST
2281
+ // Missing hour is perfectly acceptabl in subDomain, but not in domains
2282
+ if (typeof range === "number" && domains.length < range) {
2283
+ if (range > 0) {
2284
+ domains.push(this.getHourDomain(domains[domains.length-1], 2)[1]);
2285
+ } else {
2286
+ domains.shift(this.getHourDomain(domains[0], -2)[0]);
2287
+ }
2288
+ }
2289
+ return domains;
2290
+ case "day" :
2291
+ return this.getDayDomain(date, range);
2292
+ case "week" :
2293
+ return this.getWeekDomain(date, range);
2294
+ case "month":
2295
+ return this.getMonthDomain(date, range);
2296
+ case "year" :
2297
+ return this.getYearDomain(date, range);
2298
+ }
2299
+ },
2300
+
2301
+ /* jshint maxcomplexity: false */
2302
+ getSubDomain: function(date) {
2303
+ "use strict";
2304
+
2305
+ if (typeof date === "number") {
2306
+ date = new Date(date);
2307
+ }
2308
+
2309
+ var parent = this;
2310
+
2311
+ /**
2312
+ * @return int
2313
+ */
2314
+ var computeDaySubDomainSize = function(date, domain) {
2315
+ switch(domain) {
2316
+ case "year":
2317
+ return parent.getDayCountInYear(date);
2318
+ case "month":
2319
+ return parent.getDayCountInMonth(date);
2320
+ case "week":
2321
+ return 7;
2322
+ }
2323
+ };
2324
+
2325
+ /**
2326
+ * @return int
2327
+ */
2328
+ var computeMinSubDomainSize = function(date, domain) {
2329
+ switch (domain) {
2330
+ case "hour":
2331
+ return 60;
2332
+ case "day":
2333
+ return 60 * 24;
2334
+ case "week":
2335
+ return 60 * 24 * 7;
2336
+ }
2337
+ };
2338
+
2339
+ /**
2340
+ * @return int
2341
+ */
2342
+ var computeHourSubDomainSize = function(date, domain) {
2343
+ switch(domain) {
2344
+ case "day":
2345
+ return 24;
2346
+ case "week":
2347
+ return 168;
2348
+ case "month":
2349
+ return parent.getDayCountInMonth(date) * 24;
2350
+ }
2351
+ };
2352
+
2353
+ /**
2354
+ * @return int
2355
+ */
2356
+ var computeWeekSubDomainSize = function(date, domain) {
2357
+ if (domain === "month") {
2358
+ var endOfMonth = new Date(date.getFullYear(), date.getMonth()+1, 0);
2359
+ var endWeekNb = parent.getWeekNumber(endOfMonth);
2360
+ var startWeekNb = parent.getWeekNumber(new Date(date.getFullYear(), date.getMonth()));
2361
+
2362
+ if (startWeekNb > endWeekNb) {
2363
+ startWeekNb = 0;
2364
+ endWeekNb++;
2365
+ }
2366
+
2367
+ return endWeekNb - startWeekNb + 1;
2368
+ } else if (domain === "year") {
2369
+ return parent.getWeekNumber(new Date(date.getFullYear(), 11, 31));
2370
+ }
2371
+ };
2372
+
2373
+ switch(this.options.subDomain) {
2374
+ case "x_min":
2375
+ case "min" :
2376
+ return this.getMinuteDomain(date, computeMinSubDomainSize(date, this.options.domain));
2377
+ case "x_hour":
2378
+ case "hour" :
2379
+ return this.getHourDomain(date, computeHourSubDomainSize(date, this.options.domain));
2380
+ case "x_day":
2381
+ case "day" :
2382
+ return this.getDayDomain(date, computeDaySubDomainSize(date, this.options.domain));
2383
+ case "x_week":
2384
+ case "week" :
2385
+ return this.getWeekDomain(date, computeWeekSubDomainSize(date, this.options.domain));
2386
+ case "x_month":
2387
+ case "month":
2388
+ return this.getMonthDomain(date, 12);
2389
+ }
2390
+ },
2391
+
2392
+ /**
2393
+ * Get the n-th next domain after the calendar newest (rightmost) domain
2394
+ * @param int n
2395
+ * @return Date The start date of the wanted domain
2396
+ */
2397
+ getNextDomain: function(n) {
2398
+ "use strict";
2399
+
2400
+ if (arguments.length === 0) {
2401
+ n = 1;
2402
+ }
2403
+ return this.getDomain(this.jumpDate(this.getDomainKeys().pop(), n, this.options.domain), 1)[0];
2404
+ },
2405
+
2406
+ /**
2407
+ * Get the n-th domain before the calendar oldest (leftmost) domain
2408
+ * @param int n
2409
+ * @return Date The start date of the wanted domain
2410
+ */
2411
+ getPreviousDomain: function(n) {
2412
+ "use strict";
2413
+
2414
+ if (arguments.length === 0) {
2415
+ n = 1;
2416
+ }
2417
+ return this.getDomain(this.jumpDate(this.getDomainKeys().shift(), -n, this.options.domain), 1)[0];
2418
+ },
2419
+
2420
+
2421
+ // =========================================================================//
2422
+ // DATAS //
2423
+ // =========================================================================//
2424
+
2425
+ /**
2426
+ * Fetch and interpret data from the datasource
2427
+ *
2428
+ * @param string|object source
2429
+ * @param Date startDate
2430
+ * @param Date endDate
2431
+ * @param function callback
2432
+ * @param function|boolean afterLoad function used to convert the data into a json object. Use true to use the afterLoad callback
2433
+ * @param updateMode
2434
+ *
2435
+ * @return mixed
2436
+ * - True if there are no data to load
2437
+ * - False if data are loaded asynchronously
2438
+ */
2439
+ getDatas: function(source, startDate, endDate, callback, afterLoad, updateMode) {
2440
+ "use strict";
2441
+
2442
+ var self = this;
2443
+ if (arguments.length < 5) {
2444
+ afterLoad = true;
2445
+ }
2446
+ if (arguments.length < 6) {
2447
+ updateMode = this.APPEND_ON_UPDATE;
2448
+ }
2449
+ var _callback = function(data) {
2450
+ if (afterLoad !== false) {
2451
+ if (typeof afterLoad === "function") {
2452
+ data = afterLoad(data);
2453
+ } else if (typeof (self.options.afterLoadData) === "function") {
2454
+ data = self.options.afterLoadData(data);
2455
+ } else {
2456
+ console.log("Provided callback for afterLoadData is not a function.");
2457
+ }
2458
+ } else if (self.options.dataType === "csv" || self.options.dataType === "tsv") {
2459
+ data = this.interpretCSV(data);
2460
+ }
2461
+ self.parseDatas(data, updateMode, startDate, endDate);
2462
+ if (typeof callback === "function") {
2463
+ callback();
2464
+ }
2465
+ };
2466
+
2467
+ switch(typeof source) {
2468
+ case "string":
2469
+ if (source === "") {
2470
+ _callback({});
2471
+ return true;
2472
+ } else {
2473
+ switch(this.options.dataType) {
2474
+ case "json":
2475
+ d3.json(this.parseURI(source, startDate, endDate), _callback);
2476
+ break;
2477
+ case "csv":
2478
+ d3.csv(this.parseURI(source, startDate, endDate), _callback);
2479
+ break;
2480
+ case "tsv":
2481
+ d3.tsv(this.parseURI(source, startDate, endDate), _callback);
2482
+ break;
2483
+ case "txt":
2484
+ d3.text(this.parseURI(source, startDate, endDate), "text/plain", _callback);
2485
+ break;
2486
+ }
2487
+ }
2488
+ return false;
2489
+ case "object":
2490
+ if (source === Object(source)) {
2491
+ _callback(source);
2492
+ return false;
2493
+ }
2494
+ /* falls through */
2495
+ default:
2496
+ _callback({});
2497
+ return true;
2498
+ }
2499
+ },
2500
+
2501
+ /**
2502
+ * Populate the calendar internal data
2503
+ *
2504
+ * @param object data
2505
+ * @param constant updateMode
2506
+ * @param Date startDate
2507
+ * @param Date endDate
2508
+ *
2509
+ * @return void
2510
+ */
2511
+ parseDatas: function(data, updateMode, startDate, endDate) {
2512
+ "use strict";
2513
+
2514
+ if (updateMode === this.RESET_ALL_ON_UPDATE) {
2515
+ this._domains.forEach(function(key, value) {
2516
+ value.forEach(function(element, index, array) {
2517
+ array[index].v = null;
2518
+ });
2519
+ });
2520
+ }
2521
+
2522
+ var temp = {};
2523
+
2524
+ var extractTime = function(d) { return d.t; };
2525
+
2526
+ /*jshint forin:false */
2527
+ for (var d in data) {
2528
+ var date = new Date(d*1000);
2529
+ var domainUnit = this.getDomain(date)[0].getTime();
2530
+
2531
+ // The current data belongs to a domain that was compressed
2532
+ // Compress the data for the two duplicate hours into the same hour
2533
+ if (this.DSTDomain.indexOf(domainUnit) >= 0) {
2534
+
2535
+ // Re-assign all data to the first or the second duplicate hours
2536
+ // depending on which is visible
2537
+ if (this._domains.has(domainUnit - 3600 * 1000)) {
2538
+ domainUnit -= 3600 * 1000;
2539
+ }
2540
+ }
2541
+
2542
+ // Skip if data is not relevant to current domain
2543
+ if (isNaN(d) || !data.hasOwnProperty(d) || !this._domains.has(domainUnit) || !(domainUnit >= +startDate && domainUnit < +endDate)) {
2544
+ continue;
2545
+ }
2546
+
2547
+ var subDomainsData = this._domains.get(domainUnit);
2548
+
2549
+ if (!temp.hasOwnProperty(domainUnit)) {
2550
+ temp[domainUnit] = subDomainsData.map(extractTime);
2551
+ }
2552
+
2553
+ var index = temp[domainUnit].indexOf(this._domainType[this.options.subDomain].extractUnit(date));
2554
+
2555
+ if (updateMode === this.RESET_SINGLE_ON_UPDATE) {
2556
+ subDomainsData[index].v = data[d];
2557
+ } else {
2558
+ if (!isNaN(subDomainsData[index].v)) {
2559
+ subDomainsData[index].v += data[d];
2560
+ } else {
2561
+ subDomainsData[index].v = data[d];
2562
+ }
2563
+ }
2564
+ }
2565
+ },
2566
+
2567
+ parseURI: function(str, startDate, endDate) {
2568
+ "use strict";
2569
+
2570
+ // Use a timestamp in seconds
2571
+ str = str.replace(/\{\{t:start\}\}/g, startDate.getTime()/1000);
2572
+ str = str.replace(/\{\{t:end\}\}/g, endDate.getTime()/1000);
2573
+
2574
+ // Use a string date, following the ISO-8601
2575
+ str = str.replace(/\{\{d:start\}\}/g, startDate.toISOString());
2576
+ str = str.replace(/\{\{d:end\}\}/g, endDate.toISOString());
2577
+
2578
+ return str;
2579
+ },
2580
+
2581
+ interpretCSV: function(data) {
2582
+ "use strict";
2583
+
2584
+ var d = {};
2585
+ var keys = Object.keys(data[0]);
2586
+ var i, total;
2587
+ for (i = 0, total = data.length; i < total; i++) {
2588
+ d[data[i][keys[0]]] = +data[i][keys[1]];
2589
+ }
2590
+ return d;
2591
+ },
2592
+
2593
+ /**
2594
+ * Handle the calendar layout and dimension
2595
+ *
2596
+ * Expand and shrink the container depending on its children dimension
2597
+ * Also rearrange the children position depending on their dimension,
2598
+ * and the legend position
2599
+ *
2600
+ * @return void
2601
+ */
2602
+ resize: function() {
2603
+ "use strict";
2604
+
2605
+ var parent = this;
2606
+ var options = parent.options;
2607
+ var legendWidth = options.displayLegend ? (parent.Legend.getDim("width") + options.legendMargin[1] + options.legendMargin[3]) : 0;
2608
+ var legendHeight = options.displayLegend ? (parent.Legend.getDim("height") + options.legendMargin[0] + options.legendMargin[2]) : 0;
2609
+
2610
+ var graphWidth = parent.graphDim.width - options.domainGutter - options.cellPadding;
2611
+ var graphHeight = parent.graphDim.height - options.domainGutter - options.cellPadding;
2612
+
2613
+ this.root.transition().duration(options.animationDuration)
2614
+ .attr("width", function() {
2615
+ if (options.legendVerticalPosition === "middle" || options.legendVerticalPosition === "center") {
2616
+ return graphWidth + legendWidth;
2617
+ }
2618
+ return Math.max(graphWidth, legendWidth);
2619
+ })
2620
+ .attr("height", function() {
2621
+ if (options.legendVerticalPosition === "middle" || options.legendVerticalPosition === "center") {
2622
+ return Math.max(graphHeight, legendHeight);
2623
+ }
2624
+ return graphHeight + legendHeight;
2625
+ })
2626
+ ;
2627
+
2628
+ this.root.select(".graph").transition().duration(options.animationDuration)
2629
+ .attr("y", function() {
2630
+ if (options.legendVerticalPosition === "top") {
2631
+ return legendHeight;
2632
+ }
2633
+ return 0;
2634
+ })
2635
+ .attr("x", function() {
2636
+ if (
2637
+ (options.legendVerticalPosition === "middle" || options.legendVerticalPosition === "center") &&
2638
+ options.legendHorizontalPosition === "left") {
2639
+ return legendWidth;
2640
+ }
2641
+ return 0;
2642
+
2643
+ })
2644
+ ;
2645
+ },
2646
+
2647
+ // =========================================================================//
2648
+ // PUBLIC API //
2649
+ // =========================================================================//
2650
+
2651
+ /**
2652
+ * Shift the calendar forward
2653
+ */
2654
+ next: function(n) {
2655
+ "use strict";
2656
+
2657
+ if (arguments.length === 0) {
2658
+ n = 1;
2659
+ }
2660
+ return this.loadNextDomain(n);
2661
+ },
2662
+
2663
+ /**
2664
+ * Shift the calendar backward
2665
+ */
2666
+ previous: function(n) {
2667
+ "use strict";
2668
+
2669
+ if (arguments.length === 0) {
2670
+ n = 1;
2671
+ }
2672
+ return this.loadPreviousDomain(n);
2673
+ },
2674
+
2675
+ /**
2676
+ * Jump directly to a specific date
2677
+ *
2678
+ * JumpTo will scroll the calendar until the wanted domain with the specified
2679
+ * date is visible. Unless you set reset to true, the wanted domain
2680
+ * will not necessarily be the first (leftmost) domain of the calendar.
2681
+ *
2682
+ * @param Date date Jump to the domain containing that date
2683
+ * @param bool reset Whether the wanted domain should be the first domain of the calendar
2684
+ * @param bool True of the calendar was scrolled
2685
+ */
2686
+ jumpTo: function(date, reset) {
2687
+ "use strict";
2688
+
2689
+ if (arguments.length < 2) {
2690
+ reset = false;
2691
+ }
2692
+ var domains = this.getDomainKeys();
2693
+ var firstDomain = domains[0];
2694
+ var lastDomain = domains[domains.length-1];
2695
+
2696
+ if (date < firstDomain) {
2697
+ return this.loadPreviousDomain(this.getDomain(firstDomain, date).length);
2698
+ } else {
2699
+ if (reset) {
2700
+ return this.loadNextDomain(this.getDomain(firstDomain, date).length);
2701
+ }
2702
+
2703
+ if (date > lastDomain) {
2704
+ return this.loadNextDomain(this.getDomain(lastDomain, date).length);
2705
+ }
2706
+ }
2707
+
2708
+ return false;
2709
+ },
2710
+
2711
+ /**
2712
+ * Navigate back to the start date
2713
+ *
2714
+ * @since 3.3.8
2715
+ * @return void
2716
+ */
2717
+ rewind: function() {
2718
+ "use strict";
2719
+
2720
+ this.jumpTo(this.options.start, true);
2721
+ },
2722
+
2723
+ /**
2724
+ * Update the calendar with new data
2725
+ *
2726
+ * @param object|string dataSource The calendar's datasource, same type as this.options.data
2727
+ * @param boolean|function afterLoad Whether to execute afterLoad() on the data. Pass directly a function
2728
+ * if you don't want to use the afterLoad() callback
2729
+ */
2730
+ update: function(dataSource, afterLoad, updateMode) {
2731
+ "use strict";
2732
+
2733
+ if (arguments.length < 2) {
2734
+ afterLoad = true;
2735
+ }
2736
+ if (arguments.length < 3) {
2737
+ updateMode = this.RESET_ALL_ON_UPDATE;
2738
+ }
2739
+
2740
+ var domains = this.getDomainKeys();
2741
+ var self = this;
2742
+ this.getDatas(
2743
+ dataSource,
2744
+ new Date(domains[0]),
2745
+ this.getSubDomain(domains[domains.length-1]).pop(),
2746
+ function() {
2747
+ self.fill();
2748
+ },
2749
+ afterLoad,
2750
+ updateMode
2751
+ );
2752
+ },
2753
+
2754
+ /**
2755
+ * Set the legend
2756
+ *
2757
+ * @param array legend an array of integer, representing the different threshold value
2758
+ * @param array colorRange an array of 2 hex colors, for the minimum and maximum colors
2759
+ */
2760
+ setLegend: function() {
2761
+ "use strict";
2762
+
2763
+ var oldLegend = this.options.legend.slice(0);
2764
+ if (arguments.length >= 1 && Array.isArray(arguments[0])) {
2765
+ this.options.legend = arguments[0];
2766
+ }
2767
+ if (arguments.length >= 2) {
2768
+ if (Array.isArray(arguments[1]) && arguments[1].length >= 2) {
2769
+ this.options.legendColors = [arguments[1][0], arguments[1][1]];
2770
+ } else {
2771
+ this.options.legendColors = arguments[1];
2772
+ }
2773
+ }
2774
+
2775
+ if ((arguments.length > 0 && !arrayEquals(oldLegend, this.options.legend)) || arguments.length >= 2) {
2776
+ this.Legend.buildColors();
2777
+ this.fill();
2778
+ }
2779
+
2780
+ this.Legend.redraw(this.graphDim.width - this.options.domainGutter - this.options.cellPadding);
2781
+ },
2782
+
2783
+ /**
2784
+ * Remove the legend
2785
+ *
2786
+ * @return bool False if there is no legend to remove
2787
+ */
2788
+ removeLegend: function() {
2789
+ "use strict";
2790
+
2791
+ if (!this.options.displayLegend) {
2792
+ return false;
2793
+ }
2794
+ this.options.displayLegend = false;
2795
+ this.Legend.remove();
2796
+ return true;
2797
+ },
2798
+
2799
+ /**
2800
+ * Display the legend
2801
+ *
2802
+ * @return bool False if the legend was already displayed
2803
+ */
2804
+ showLegend: function() {
2805
+ "use strict";
2806
+
2807
+ if (this.options.displayLegend) {
2808
+ return false;
2809
+ }
2810
+ this.options.displayLegend = true;
2811
+ this.Legend.redraw(this.graphDim.width - this.options.domainGutter - this.options.cellPadding);
2812
+ return true;
2813
+ },
2814
+
2815
+ /**
2816
+ * Highlight dates
2817
+ *
2818
+ * Add a highlight class to a set of dates
2819
+ *
2820
+ * @since 3.3.5
2821
+ * @param array Array of dates to highlight
2822
+ * @return bool True if dates were highlighted
2823
+ */
2824
+ highlight: function(args) {
2825
+ "use strict";
2826
+
2827
+ if ((this.options.highlight = this.expandDateSetting(args)).length > 0) {
2828
+ this.fill();
2829
+ return true;
2830
+ }
2831
+ return false;
2832
+ },
2833
+
2834
+ /**
2835
+ * Destroy the calendar
2836
+ *
2837
+ * Usage: cal = cal.destroy();
2838
+ *
2839
+ * @since 3.3.6
2840
+ * @param function A callback function to trigger after destroying the calendar
2841
+ * @return null
2842
+ */
2843
+ destroy: function(callback) {
2844
+ "use strict";
2845
+
2846
+ this.root.transition().duration(this.options.animationDuration)
2847
+ .attr("width", 0)
2848
+ .attr("height", 0)
2849
+ .remove()
2850
+ .each("end", function() {
2851
+ if (typeof callback === "function") {
2852
+ callback();
2853
+ } else if (arguments.length > 0) {
2854
+ console.log("Provided callback for destroy() is not a function.");
2855
+ }
2856
+ })
2857
+ ;
2858
+
2859
+ return null;
2860
+ },
2861
+
2862
+ getSVG: function() {
2863
+ "use strict";
2864
+
2865
+ var styles = {
2866
+ ".cal-heatmap-container": {},
2867
+ ".graph": {},
2868
+ ".graph-rect": {},
2869
+ "rect.highlight": {},
2870
+ "rect.now": {},
2871
+ "text.highlight": {},
2872
+ "text.now": {},
2873
+ ".domain-background": {},
2874
+ ".graph-label": {},
2875
+ ".subdomain-text": {},
2876
+ ".q0": {},
2877
+ ".qi": {}
2878
+ };
2879
+
2880
+ for (var j = 1, total = this.options.legend.length+1; j <= total; j++) {
2881
+ styles[".q" + j] = {};
2882
+ }
2883
+
2884
+ var root = this.root;
2885
+
2886
+ var whitelistStyles = [
2887
+ // SVG specific properties
2888
+ "stroke", "stroke-width", "stroke-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-miterlimit",
2889
+ "fill", "fill-opacity", "fill-rule",
2890
+ "marker", "marker-start", "marker-mid", "marker-end",
2891
+ "alignement-baseline", "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal", "glyph-orientation-vertical", "kerning", "text-anchor",
2892
+ "shape-rendering",
2893
+
2894
+ // Text Specific properties
2895
+ "text-transform", "font-family", "font", "font-size", "font-weight"
2896
+ ];
2897
+
2898
+ var filterStyles = function(attribute, property, value) {
2899
+ if (whitelistStyles.indexOf(property) !== -1) {
2900
+ styles[attribute][property] = value;
2901
+ }
2902
+ };
2903
+
2904
+ var getElement = function(e) {
2905
+ return root.select(e)[0][0];
2906
+ };
2907
+
2908
+ /* jshint forin:false */
2909
+ for (var element in styles) {
2910
+ if (!styles.hasOwnProperty(element)) {
2911
+ continue;
2912
+ }
2913
+
2914
+ var dom = getElement(element);
2915
+
2916
+ if (dom === null) {
2917
+ continue;
2918
+ }
2919
+
2920
+ // The DOM Level 2 CSS way
2921
+ /* jshint maxdepth: false */
2922
+ if ("getComputedStyle" in window) {
2923
+ var cs = getComputedStyle(dom, null);
2924
+ if (cs.length !== 0) {
2925
+ for (var i = 0; i < cs.length; i++) {
2926
+ filterStyles(element, cs.item(i), cs.getPropertyValue(cs.item(i)));
2927
+ }
2928
+
2929
+ // Opera workaround. Opera doesn"t support `item`/`length`
2930
+ // on CSSStyleDeclaration.
2931
+ } else {
2932
+ for (var k in cs) {
2933
+ if (cs.hasOwnProperty(k)) {
2934
+ filterStyles(element, k, cs[k]);
2935
+ }
2936
+ }
2937
+ }
2938
+
2939
+ // The IE way
2940
+ } else if ("currentStyle" in dom) {
2941
+ var css = dom.currentStyle;
2942
+ for (var p in css) {
2943
+ filterStyles(element, p, css[p]);
2944
+ }
2945
+ }
2946
+ }
2947
+
2948
+ var string = "<svg xmlns=\"http://www.w3.org/2000/svg\" "+
2949
+ "xmlns:xlink=\"http://www.w3.org/1999/xlink\"><style type=\"text/css\"><![CDATA[ ";
2950
+
2951
+ for (var style in styles) {
2952
+ string += style + " {\n";
2953
+ for (var l in styles[style]) {
2954
+ string += "\t" + l + ":" + styles[style][l] + ";\n";
2955
+ }
2956
+ string += "}\n";
2957
+ }
2958
+
2959
+ string += "]]></style>";
2960
+ string += new XMLSerializer().serializeToString(this.root[0][0]);
2961
+ string += "</svg>";
2962
+
2963
+ return string;
2964
+ }
2965
+ };
2966
+
2967
+ // =========================================================================//
2968
+ // DOMAIN POSITION COMPUTATION //
2969
+ // =========================================================================//
2970
+
2971
+ /**
2972
+ * Compute the position of a domain, relative to the calendar
2973
+ */
2974
+ var DomainPosition = function() {
2975
+ "use strict";
2976
+
2977
+ this.positions = d3.map();
2978
+ };
2979
+
2980
+ DomainPosition.prototype.getPosition = function(d) {
2981
+ "use strict";
2982
+
2983
+ return this.positions.get(d);
2984
+ };
2985
+
2986
+ DomainPosition.prototype.getPositionFromIndex = function(i) {
2987
+ "use strict";
2988
+
2989
+ var domains = this.getKeys();
2990
+ return this.positions.get(domains[i]);
2991
+ };
2992
+
2993
+ DomainPosition.prototype.getLast = function() {
2994
+ "use strict";
2995
+
2996
+ var domains = this.getKeys();
2997
+ return this.positions.get(domains[domains.length-1]);
2998
+ };
2999
+
3000
+ DomainPosition.prototype.setPosition = function(d, dim) {
3001
+ "use strict";
3002
+
3003
+ this.positions.set(d, dim);
3004
+ };
3005
+
3006
+ DomainPosition.prototype.shiftRightBy = function(exitingDomainDim) {
3007
+ "use strict";
3008
+
3009
+ this.positions.forEach(function(key, value) {
3010
+ this.set(key, value - exitingDomainDim);
3011
+ });
3012
+
3013
+ var domains = this.getKeys();
3014
+ this.positions.remove(domains[0]);
3015
+ };
3016
+
3017
+ DomainPosition.prototype.shiftLeftBy = function(enteringDomainDim) {
3018
+ "use strict";
3019
+
3020
+ this.positions.forEach(function(key, value) {
3021
+ this.set(key, value + enteringDomainDim);
3022
+ });
3023
+
3024
+ var domains = this.getKeys();
3025
+ this.positions.remove(domains[domains.length-1]);
3026
+ };
3027
+
3028
+ DomainPosition.prototype.getKeys = function() {
3029
+ "use strict";
3030
+
3031
+ return this.positions.keys().sort(function(a, b) {
3032
+ return parseInt(a, 10) - parseInt(b, 10);
3033
+ });
3034
+ };
3035
+
3036
+ // =========================================================================//
3037
+ // LEGEND //
3038
+ // =========================================================================//
3039
+
3040
+ var Legend = function(calendar) {
3041
+ "use strict";
3042
+
3043
+ this.calendar = calendar;
3044
+ this.computeDim();
3045
+
3046
+ if (calendar.options.legendColors !== null) {
3047
+ this.buildColors();
3048
+ }
3049
+ };
3050
+
3051
+ Legend.prototype.computeDim = function() {
3052
+ "use strict";
3053
+
3054
+ var options = this.calendar.options; // Shorter accessor for variable name mangling when minifying
3055
+ this.dim = {
3056
+ width:
3057
+ options.legendCellSize * (options.legend.length+1) +
3058
+ options.legendCellPadding * (options.legend.length),
3059
+ height:
3060
+ options.legendCellSize
3061
+ };
3062
+ };
3063
+
3064
+ Legend.prototype.remove = function() {
3065
+ "use strict";
3066
+
3067
+ this.calendar.root.select(".graph-legend").remove();
3068
+ this.calendar.resize();
3069
+ };
3070
+
3071
+ Legend.prototype.redraw = function(width) {
3072
+ "use strict";
3073
+
3074
+ if (!this.calendar.options.displayLegend) {
3075
+ return false;
3076
+ }
3077
+
3078
+ var parent = this;
3079
+ var calendar = this.calendar;
3080
+ var legend = calendar.root;
3081
+ var legendItem;
3082
+ var options = calendar.options; // Shorter accessor for variable name mangling when minifying
3083
+
3084
+ this.computeDim();
3085
+
3086
+ var _legend = options.legend.slice(0);
3087
+ _legend.push(_legend[_legend.length-1]+1);
3088
+
3089
+ var legendElement = calendar.root.select(".graph-legend");
3090
+ if (legendElement[0][0] !== null) {
3091
+ legend = legendElement;
3092
+ legendItem = legend
3093
+ .select("g")
3094
+ .selectAll("rect").data(_legend)
3095
+ ;
3096
+ } else {
3097
+ // Creating the new legend DOM if it doesn't already exist
3098
+ legend = options.legendVerticalPosition === "top" ? legend.insert("svg", ".graph") : legend.append("svg");
3099
+
3100
+ legend
3101
+ .attr("x", getLegendXPosition())
3102
+ .attr("y", getLegendYPosition())
3103
+ ;
3104
+
3105
+ legendItem = legend
3106
+ .attr("class", "graph-legend")
3107
+ .attr("height", parent.getDim("height"))
3108
+ .attr("width", parent.getDim("width"))
3109
+ .append("g")
3110
+ .selectAll().data(_legend)
3111
+ ;
3112
+ }
3113
+
3114
+ legendItem
3115
+ .enter()
3116
+ .append("rect")
3117
+ .call(legendCellLayout)
3118
+ .attr("class", function(d){ return calendar.Legend.getClass(d, (calendar.legendScale === null)); })
3119
+ .attr("fill-opacity", 0)
3120
+ .call(function(selection) {
3121
+ if (calendar.legendScale !== null && options.legendColors !== null && options.legendColors.hasOwnProperty("base")) {
3122
+ selection.attr("fill", options.legendColors.base);
3123
+ }
3124
+ })
3125
+ .append("title")
3126
+ ;
3127
+
3128
+ legendItem.exit().transition().duration(options.animationDuration)
3129
+ .attr("fill-opacity", 0)
3130
+ .remove();
3131
+
3132
+ legendItem.transition().delay(function(d, i) { return options.animationDuration * i/10; })
3133
+ .call(legendCellLayout)
3134
+ .attr("fill-opacity", 1)
3135
+ .call(function(element) {
3136
+ element.attr("fill", function(d, i) {
3137
+ if (calendar.legendScale === null) {
3138
+ return "";
3139
+ }
3140
+
3141
+ if (i === 0) {
3142
+ return calendar.legendScale(d - 1);
3143
+ }
3144
+ return calendar.legendScale(options.legend[i-1]);
3145
+ });
3146
+
3147
+ element.attr("class", function(d) { return calendar.Legend.getClass(d, (calendar.legendScale === null)); });
3148
+ })
3149
+ ;
3150
+
3151
+ function legendCellLayout(selection) {
3152
+ selection
3153
+ .attr("width", options.legendCellSize)
3154
+ .attr("height", options.legendCellSize)
3155
+ .attr("x", function(d, i) {
3156
+ return i * (options.legendCellSize + options.legendCellPadding);
3157
+ })
3158
+ ;
3159
+ }
3160
+
3161
+ legendItem.select("title").text(function(d, i) {
3162
+ if (i === 0) {
3163
+ return (options.legendTitleFormat.lower).format({
3164
+ min: options.legend[i],
3165
+ name: options.itemName[1]
3166
+ });
3167
+ } else if (i === _legend.length-1) {
3168
+ return (options.legendTitleFormat.upper).format({
3169
+ max: options.legend[i-1],
3170
+ name: options.itemName[1]
3171
+ });
3172
+ } else {
3173
+ return (options.legendTitleFormat.inner).format({
3174
+ down: options.legend[i-1],
3175
+ up: options.legend[i],
3176
+ name: options.itemName[1]
3177
+ });
3178
+ }
3179
+ })
3180
+ ;
3181
+
3182
+ legend.transition().duration(options.animationDuration)
3183
+ .attr("x", getLegendXPosition())
3184
+ .attr("y", getLegendYPosition())
3185
+ .attr("width", parent.getDim("width"))
3186
+ .attr("height", parent.getDim("height"))
3187
+ ;
3188
+
3189
+ legend.select("g").transition().duration(options.animationDuration)
3190
+ .attr("transform", function() {
3191
+ if (options.legendOrientation === "vertical") {
3192
+ return "rotate(90 " + options.legendCellSize/2 + " " + options.legendCellSize/2 + ")";
3193
+ }
3194
+ return "";
3195
+ })
3196
+ ;
3197
+
3198
+ function getLegendXPosition() {
3199
+ switch(options.legendHorizontalPosition) {
3200
+ case "right":
3201
+ if (options.legendVerticalPosition === "center" || options.legendVerticalPosition === "middle") {
3202
+ return width + options.legendMargin[3];
3203
+ }
3204
+ return width - parent.getDim("width") - options.legendMargin[1];
3205
+ case "middle":
3206
+ case "center":
3207
+ return Math.round(width/2 - parent.getDim("width")/2);
3208
+ default:
3209
+ return options.legendMargin[3];
3210
+ }
3211
+ }
3212
+
3213
+ function getLegendYPosition() {
3214
+ if (options.legendVerticalPosition === "bottom") {
3215
+ return calendar.graphDim.height + options.legendMargin[0] - options.domainGutter - options.cellPadding;
3216
+ }
3217
+ return options.legendMargin[0];
3218
+ }
3219
+
3220
+ calendar.resize();
3221
+ };
3222
+
3223
+ /**
3224
+ * Return the dimension of the legend
3225
+ *
3226
+ * Takes into account rotation
3227
+ *
3228
+ * @param string axis Width or height
3229
+ * @return int height or width in pixels
3230
+ */
3231
+ Legend.prototype.getDim = function(axis) {
3232
+ "use strict";
3233
+
3234
+ var isHorizontal = (this.calendar.options.legendOrientation === "horizontal");
3235
+
3236
+ switch(axis) {
3237
+ case "width":
3238
+ return this.dim[isHorizontal ? "width": "height"];
3239
+ case "height":
3240
+ return this.dim[isHorizontal ? "height": "width"];
3241
+ }
3242
+ };
3243
+
3244
+ Legend.prototype.buildColors = function() {
3245
+ "use strict";
3246
+
3247
+ var options = this.calendar.options; // Shorter accessor for variable name mangling when minifying
3248
+
3249
+ if (options.legendColors === null) {
3250
+ this.calendar.legendScale = null;
3251
+ return false;
3252
+ }
3253
+
3254
+ var _colorRange = [];
3255
+
3256
+ if (Array.isArray(options.legendColors)) {
3257
+ _colorRange = options.legendColors;
3258
+ } else if (options.legendColors.hasOwnProperty("min") && options.legendColors.hasOwnProperty("max")) {
3259
+ _colorRange = [options.legendColors.min, options.legendColors.max];
3260
+ } else {
3261
+ options.legendColors = null;
3262
+ return false;
3263
+ }
3264
+
3265
+ var _legend = options.legend.slice(0);
3266
+
3267
+ if (_legend[0] > 0) {
3268
+ _legend.unshift(0);
3269
+ } else if (_legend[0] < 0) {
3270
+ // Let's guess the leftmost value, it we have to add one
3271
+ _legend.unshift(_legend[0] - (_legend[_legend.length-1] - _legend[0])/_legend.length);
3272
+ }
3273
+
3274
+ var colorScale = d3.scale.linear()
3275
+ .range(_colorRange)
3276
+ .interpolate(d3.interpolateHcl)
3277
+ .domain([d3.min(_legend), d3.max(_legend)])
3278
+ ;
3279
+
3280
+ var legendColors = _legend.map(function(element) { return colorScale(element); });
3281
+ this.calendar.legendScale = d3.scale.threshold().domain(options.legend).range(legendColors);
3282
+
3283
+ return true;
3284
+ };
3285
+
3286
+ /**
3287
+ * Return the classname on the legend for the specified value
3288
+ *
3289
+ * @param integer n Value associated to a date
3290
+ * @param bool withCssClass Whether to display the css class used to style the cell.
3291
+ * Disabling will allow styling directly via html fill attribute
3292
+ *
3293
+ * @return string Classname according to the legend
3294
+ */
3295
+ Legend.prototype.getClass = function(n, withCssClass) {
3296
+ "use strict";
3297
+
3298
+ if (n === null || isNaN(n)) {
3299
+ return "";
3300
+ }
3301
+
3302
+ var index = [this.calendar.options.legend.length + 1];
3303
+
3304
+ for (var i = 0, total = this.calendar.options.legend.length-1; i <= total; i++) {
3305
+
3306
+ if (this.calendar.options.legend[0] > 0 && n < 0) {
3307
+ index = ["1", "i"];
3308
+ break;
3309
+ }
3310
+
3311
+ if (n <= this.calendar.options.legend[i]) {
3312
+ index = [i+1];
3313
+ break;
3314
+ }
3315
+ }
3316
+
3317
+ if (n === 0) {
3318
+ index.push(0);
3319
+ }
3320
+
3321
+ index.unshift("");
3322
+ return (index.join(" r") + (withCssClass ? index.join(" q"): "")).trim();
3323
+ };
3324
+
3325
+ /**
3326
+ * Sprintf like function
3327
+ * @source http://stackoverflow.com/a/4795914/805649
3328
+ * @return String
3329
+ */
3330
+ String.prototype.format = function () {
3331
+ "use strict";
3332
+
3333
+ var formatted = this;
3334
+ for (var prop in arguments[0]) {
3335
+ if (arguments[0].hasOwnProperty(prop)) {
3336
+ var regexp = new RegExp("\\{" + prop + "\\}", "gi");
3337
+ formatted = formatted.replace(regexp, arguments[0][prop]);
3338
+ }
3339
+ }
3340
+ return formatted;
3341
+ };
3342
+
3343
+ /**
3344
+ * #source http://stackoverflow.com/a/383245/805649
3345
+ */
3346
+ function mergeRecursive(obj1, obj2) {
3347
+ "use strict";
3348
+
3349
+ /*jshint forin:false */
3350
+ for (var p in obj2) {
3351
+ try {
3352
+ // Property in destination object set; update its value.
3353
+ if (obj2[p].constructor === Object) {
3354
+ obj1[p] = mergeRecursive(obj1[p], obj2[p]);
3355
+ } else {
3356
+ obj1[p] = obj2[p];
3357
+ }
3358
+ } catch(e) {
3359
+ // Property in destination object not set; create it and set its value.
3360
+ obj1[p] = obj2[p];
3361
+ }
3362
+ }
3363
+
3364
+ return obj1;
3365
+ }
3366
+
3367
+ /**
3368
+ * Check if 2 arrays are equals
3369
+ *
3370
+ * @link http://stackoverflow.com/a/14853974/805649
3371
+ * @param array array the array to compare to
3372
+ * @return bool true of the 2 arrays are equals
3373
+ */
3374
+ function arrayEquals(arrayA, arrayB) {
3375
+ "use strict";
3376
+
3377
+ // if the other array is a falsy value, return
3378
+ if (!arrayB || !arrayA) {
3379
+ return false;
3380
+ }
3381
+
3382
+ // compare lengths - can save a lot of time
3383
+ if (arrayA.length !== arrayB.length) {
3384
+ return false;
3385
+ }
3386
+
3387
+ for (var i = 0; i < arrayA.length; i++) {
3388
+ // Check if we have nested arrays
3389
+ if (arrayA[i] instanceof Array && arrayB[i] instanceof Array) {
3390
+ // recurse into the nested arrays
3391
+ if (!arrayEquals(arrayA[i], arrayB[i])) {
3392
+ return false;
3393
+ }
3394
+ }
3395
+ else if (arrayA[i] !== arrayB[i]) {
3396
+ // Warning - two different object instances will never be equal: {x:20} != {x:20}
3397
+ return false;
3398
+ }
3399
+ }
3400
+ return true;
3401
+ }
3402
+
3403
+ /**
3404
+ * AMD Loader
3405
+ */
3406
+ if (typeof define === "function" && define.amd) {
3407
+ define(["d3"], function() {
3408
+ "use strict";
3409
+
3410
+ return CalHeatMap;
3411
+ });
3412
+ }