cal-heatmap-rails 0.0.1

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