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