UNPKG

70.1 kBJavaScriptView Raw
1import { Component, Input, Output, EventEmitter, ViewChild, HostListener } from '@angular/core';
2// Import dependencies
3import moment from 'moment';
4import * as d3 from 'd3/index';
5var CalendarHeatmap = /** @class */ (function () {
6 function CalendarHeatmap() {
7 this.color = '#ff4500';
8 this.overview = 'global';
9 this.handler = new EventEmitter();
10 this.gutter = 5;
11 this.item_gutter = 1;
12 this.width = 1000;
13 this.height = 200;
14 this.item_size = 10;
15 this.label_padding = 40;
16 this.max_block_height = 20;
17 this.transition_duration = 500;
18 this.in_transition = false;
19 this.tooltip_width = 250;
20 this.tooltip_padding = 15;
21 this.history = ['global'];
22 this.selected = {};
23 }
24 /**
25 * Check if data is available
26 * @return {?}
27 */
28 CalendarHeatmap.prototype.ngOnChanges = function () {
29 if (!this.data) {
30 return;
31 }
32 // Update data summaries
33 this.updateDataSummary();
34 // Draw the chart
35 this.drawChart();
36 };
37 ;
38 /**
39 * Get hold of the root element and append our svg
40 * @return {?}
41 */
42 CalendarHeatmap.prototype.ngAfterViewInit = function () {
43 var /** @type {?} */ element = this.element.nativeElement;
44 // Initialize svg element
45 this.svg = d3.select(element)
46 .append('svg')
47 .attr('class', 'svg');
48 // Initialize main svg elements
49 this.items = this.svg.append('g');
50 this.labels = this.svg.append('g');
51 this.buttons = this.svg.append('g');
52 // Add tooltip to the same element as main svg
53 this.tooltip = d3.select(element).append('div')
54 .attr('class', 'heatmap-tooltip')
55 .style('opacity', 0);
56 // Calculate chart dimensions
57 this.calculateDimensions();
58 // Draw the chart
59 this.drawChart();
60 };
61 ;
62 /**
63 * Utility function to get number of complete weeks in a year
64 * @return {?}
65 */
66 CalendarHeatmap.prototype.getNumberOfWeeks = function () {
67 var /** @type {?} */ dayIndex = Math.round((+moment() - +moment().subtract(1, 'year').startOf('week')) / 86400000);
68 var /** @type {?} */ colIndex = Math.trunc(dayIndex / 7);
69 var /** @type {?} */ numWeeks = colIndex + 1;
70 return numWeeks;
71 };
72 ;
73 /**
74 * Utility funciton to calculate chart dimensions
75 * @return {?}
76 */
77 CalendarHeatmap.prototype.calculateDimensions = function () {
78 var /** @type {?} */ element = this.element.nativeElement;
79 this.width = element.clientWidth < 1000 ? 1000 : element.clientWidth;
80 this.item_size = ((this.width - this.label_padding) / this.getNumberOfWeeks() - this.gutter);
81 this.height = this.label_padding + 7 * (this.item_size + this.gutter);
82 this.svg.attr('width', this.width).attr('height', this.height);
83 };
84 ;
85 /**
86 * Recalculate dimensions on window resize events
87 * @param {?} event
88 * @return {?}
89 */
90 CalendarHeatmap.prototype.onResize = function (event) {
91 this.calculateDimensions();
92 if (!!this.data && !!this.data[0]['summary']) {
93 this.drawChart();
94 }
95 };
96 ;
97 /**
98 * Helper function to check for data summary
99 * @return {?}
100 */
101 CalendarHeatmap.prototype.updateDataSummary = function () {
102 // Get daily summary if that was not provided
103 if (!this.data[0]['summary']) {
104 this.data.map(function (d) {
105 var /** @type {?} */ summary = d['details'].reduce(function (uniques, project) {
106 if (!uniques[project.name]) {
107 uniques[project.name] = {
108 'value': project.value
109 };
110 }
111 else {
112 uniques[project.name].value += project.value;
113 }
114 return uniques;
115 }, {});
116 var /** @type {?} */ unsorted_summary = Object.keys(summary).map(function (key) {
117 return {
118 'name': key,
119 'value': summary[key].value
120 };
121 });
122 d['summary'] = unsorted_summary.sort(function (a, b) {
123 return b.value - a.value;
124 });
125 return d;
126 });
127 }
128 };
129 /**
130 * Draw the chart based on the current overview type
131 * @return {?}
132 */
133 CalendarHeatmap.prototype.drawChart = function () {
134 if (!this.svg || !this.data) {
135 return;
136 }
137 if (this.overview === 'global') {
138 this.drawGlobalOverview();
139 }
140 else if (this.overview === 'year') {
141 this.drawYearOverview();
142 }
143 else if (this.overview === 'month') {
144 this.drawMonthOverview();
145 }
146 else if (this.overview === 'week') {
147 this.drawWeekOverview();
148 }
149 else if (this.overview === 'day') {
150 this.drawDayOverview();
151 }
152 };
153 ;
154 /**
155 * Draw global overview (multiple years)
156 * @return {?}
157 */
158 CalendarHeatmap.prototype.drawGlobalOverview = function () {
159 var _this = this;
160 // Add current overview to the history
161 if (this.history[this.history.length - 1] !== this.overview) {
162 this.history.push(this.overview);
163 }
164 // Define start and end of the dataset
165 var /** @type {?} */ start = moment(this.data[0]['date']).startOf('year');
166 var /** @type {?} */ end = moment(this.data[this.data.length - 1]['date']).endOf('year');
167 // Define array of years and total values
168 var /** @type {?} */ data = this.data;
169 var /** @type {?} */ year_data = d3.timeYears(start, end).map(function (d) {
170 var /** @type {?} */ date = moment(d);
171 return {
172 'date': date,
173 'total': data.reduce(function (prev, current) {
174 if (moment(current.date).year() === date.year()) {
175 prev += current.total;
176 }
177 return prev;
178 }, 0),
179 'summary': function () {
180 var /** @type {?} */ summary = data.reduce(function (summary, d) {
181 if (moment(d.date).year() === date.year()) {
182 for (var /** @type {?} */ i = 0; i < d.summary.length; i++) {
183 if (!summary[d.summary[i].name]) {
184 summary[d.summary[i].name] = {
185 'value': d.summary[i].value,
186 };
187 }
188 else {
189 summary[d.summary[i].name].value += d.summary[i].value;
190 }
191 }
192 }
193 return summary;
194 }, {});
195 var /** @type {?} */ unsorted_summary = Object.keys(summary).map(function (key) {
196 return {
197 'name': key,
198 'value': summary[key].value
199 };
200 });
201 return unsorted_summary.sort(function (a, b) {
202 return b.value - a.value;
203 });
204 }(),
205 };
206 });
207 // Calculate max value of all the years in the dataset
208 var /** @type {?} */ max_value = d3.max(year_data, function (d) {
209 return d.total;
210 });
211 // Define year labels and axis
212 var /** @type {?} */ year_labels = d3.timeYears(start, end).map(function (d) {
213 return moment(d);
214 });
215 var /** @type {?} */ yearScale = d3.scaleBand()
216 .rangeRound([0, this.width])
217 .padding(0.05)
218 .domain(year_labels.map(function (d) {
219 return d.year();
220 }));
221 // Add month data items to the overview
222 this.items.selectAll('.item-block-year').remove();
223 var /** @type {?} */ item_block = this.items.selectAll('.item-block-year')
224 .data(year_data)
225 .enter()
226 .append('rect')
227 .attr('class', 'item item-block-year')
228 .attr('width', function () {
229 return (_this.width - _this.label_padding) / year_labels.length - _this.gutter * 5;
230 })
231 .attr('height', function () {
232 return _this.height - _this.label_padding;
233 })
234 .attr('transform', function (d) {
235 return 'translate(' + yearScale(d.date.year()) + ',' + _this.tooltip_padding * 2 + ')';
236 })
237 .attr('fill', function (d) {
238 var /** @type {?} */ color = d3.scaleLinear()
239 .range(['#ffffff', _this.color || '#ff4500'])
240 .domain([-0.15 * max_value, max_value]);
241 return color(d.total) || '#ff4500';
242 })
243 .on('click', function (d) {
244 if (_this.in_transition) {
245 return;
246 }
247 // Set in_transition flag
248 _this.in_transition = true;
249 // Set selected date to the one clicked on
250 _this.selected = d;
251 // Hide tooltip
252 _this.hideTooltip();
253 // Remove all month overview related items and labels
254 _this.removeGlobalOverview();
255 // Redraw the chart
256 _this.overview = 'year';
257 _this.drawChart();
258 })
259 .style('opacity', 0)
260 .on('mouseover', function (d) {
261 if (_this.in_transition) {
262 return;
263 }
264 // Construct tooltip
265 var /** @type {?} */ tooltip_html = '';
266 tooltip_html += '<div><span><strong>Total time tracked:</strong></span>';
267 var /** @type {?} */ sec = parseInt(d.total, 10);
268 var /** @type {?} */ days = Math.floor(sec / 86400);
269 if (days > 0) {
270 tooltip_html += '<span>' + (days === 1 ? '1 day' : days + ' days') + '</span></div>';
271 }
272 var /** @type {?} */ hours = Math.floor((sec - (days * 86400)) / 3600);
273 if (hours > 0) {
274 if (days > 0) {
275 tooltip_html += '<div><span></span><span>' + (hours === 1 ? '1 hour' : hours + ' hours') + '</span></div>';
276 }
277 else {
278 tooltip_html += '<span>' + (hours === 1 ? '1 hour' : hours + ' hours') + '</span></div>';
279 }
280 }
281 var /** @type {?} */ minutes = Math.floor((sec - (days * 86400) - (hours * 3600)) / 60);
282 if (minutes > 0) {
283 if (days > 0 || hours > 0) {
284 tooltip_html += '<div><span></span><span>' + (minutes === 1 ? '1 minute' : minutes + ' minutes') + '</span></div>';
285 }
286 else {
287 tooltip_html += '<span>' + (minutes === 1 ? '1 minute' : minutes + ' minutes') + '</span></div>';
288 }
289 }
290 tooltip_html += '<br />';
291 // Add summary to the tooltip
292 if (d.summary.length <= 5) {
293 for (var /** @type {?} */ i = 0; i < d.summary.length; i++) {
294 tooltip_html += '<div><span><strong>' + d.summary[i].name + '</strong></span>';
295 tooltip_html += '<span>' + _this.formatTime(d.summary[i].value) + '</span></div>';
296 }
297 ;
298 }
299 else {
300 for (var /** @type {?} */ i = 0; i < 5; i++) {
301 tooltip_html += '<div><span><strong>' + d.summary[i].name + '</strong></span>';
302 tooltip_html += '<span>' + _this.formatTime(d.summary[i].value) + '</span></div>';
303 }
304 ;
305 tooltip_html += '<br />';
306 var /** @type {?} */ other_projects_sum = 0;
307 for (var /** @type {?} */ i = 5; i < d.summary.length; i++) {
308 other_projects_sum = +d.summary[i].value;
309 }
310 ;
311 tooltip_html += '<div><span><strong>Other:</strong></span>';
312 tooltip_html += '<span>' + _this.formatTime(other_projects_sum) + '</span></div>';
313 }
314 // Calculate tooltip position
315 var /** @type {?} */ x = yearScale(d.date.year()) + _this.tooltip_padding * 2;
316 while (_this.width - x < (_this.tooltip_width + _this.tooltip_padding * 5)) {
317 x -= 10;
318 }
319 var /** @type {?} */ y = _this.tooltip_padding * 3;
320 // Show tooltip
321 _this.tooltip.html(tooltip_html)
322 .style('left', x + 'px')
323 .style('top', y + 'px')
324 .transition()
325 .duration(_this.transition_duration / 2)
326 .ease(d3.easeLinear)
327 .style('opacity', 1);
328 })
329 .on('mouseout', function () {
330 if (_this.in_transition) {
331 return;
332 }
333 _this.hideTooltip();
334 })
335 .transition()
336 .delay(function (d, i) {
337 return _this.transition_duration * (i + 1) / 10;
338 })
339 .duration(function () {
340 return _this.transition_duration;
341 })
342 .ease(d3.easeLinear)
343 .style('opacity', 1)
344 .call(function (transition, callback) {
345 if (transition.empty()) {
346 callback();
347 }
348 var /** @type {?} */ n = 0;
349 transition
350 .each(function () { ++n; })
351 .on('end', function () {
352 if (!--n) {
353 callback.apply(this, arguments);
354 }
355 });
356 }, function () {
357 _this.in_transition = false;
358 });
359 // Add year labels
360 this.labels.selectAll('.label-year').remove();
361 this.labels.selectAll('.label-year')
362 .data(year_labels)
363 .enter()
364 .append('text')
365 .attr('class', 'label label-year')
366 .attr('font-size', function () {
367 return Math.floor(_this.label_padding / 3) + 'px';
368 })
369 .text(function (d) {
370 return d.year();
371 })
372 .attr('x', function (d) {
373 return yearScale(d.year());
374 })
375 .attr('y', this.label_padding / 2)
376 .on('mouseenter', function (year_label) {
377 if (_this.in_transition) {
378 return;
379 }
380 _this.items.selectAll('.item-block-year')
381 .transition()
382 .duration(_this.transition_duration)
383 .ease(d3.easeLinear)
384 .style('opacity', function (d) {
385 return (moment(d.date).year() === year_label.year()) ? 1 : 0.1;
386 });
387 })
388 .on('mouseout', function () {
389 if (_this.in_transition) {
390 return;
391 }
392 _this.items.selectAll('.item-block-year')
393 .transition()
394 .duration(_this.transition_duration)
395 .ease(d3.easeLinear)
396 .style('opacity', 1);
397 })
398 .on('click', function (d) {
399 if (_this.in_transition) {
400 return;
401 }
402 // Set in_transition flag
403 _this.in_transition = true;
404 // Set selected month to the one clicked on
405 _this.selected = d;
406 // Hide tooltip
407 _this.hideTooltip();
408 // Remove all year overview related items and labels
409 _this.removeGlobalOverview();
410 // Redraw the chart
411 _this.overview = 'year';
412 _this.drawChart();
413 });
414 };
415 ;
416 /**
417 * Draw year overview
418 * @return {?}
419 */
420 CalendarHeatmap.prototype.drawYearOverview = function () {
421 var _this = this;
422 // Add current overview to the history
423 if (this.history[this.history.length - 1] !== this.overview) {
424 this.history.push(this.overview);
425 }
426 // Define start and end date of the selected year
427 var /** @type {?} */ start_of_year = moment(this.selected['date']).startOf('year');
428 var /** @type {?} */ end_of_year = moment(this.selected['date']).endOf('year');
429 // Filter data down to the selected year
430 var /** @type {?} */ year_data = this.data.filter(function (d) {
431 return start_of_year <= moment(d.date) && moment(d.date) < end_of_year;
432 });
433 // Calculate max value of the year data
434 var /** @type {?} */ max_value = d3.max(year_data, function (d) {
435 return d.total;
436 });
437 var /** @type {?} */ color = d3.scaleLinear()
438 .range(['#ffffff', this.color])
439 .domain([-0.15 * max_value, max_value]);
440 this.items.selectAll('.item-circle').remove();
441 this.items.selectAll('.item-circle')
442 .data(year_data)
443 .enter()
444 .append('rect')
445 .attr('class', 'item item-circle')
446 .style('opacity', 0)
447 .attr('x', function (d) {
448 return _this.calcItemX(d, start_of_year) + (_this.item_size - _this.calcItemSize(d, max_value)) / 2;
449 })
450 .attr('y', function (d) {
451 return _this.calcItemY(d) + (_this.item_size - _this.calcItemSize(d, max_value)) / 2;
452 })
453 .attr('rx', function (d) {
454 return _this.calcItemSize(d, max_value);
455 })
456 .attr('ry', function (d) {
457 return _this.calcItemSize(d, max_value);
458 })
459 .attr('width', function (d) {
460 return _this.calcItemSize(d, max_value);
461 })
462 .attr('height', function (d) {
463 return _this.calcItemSize(d, max_value);
464 })
465 .attr('fill', function (d) {
466 return (d.total > 0) ? color(d.total) : 'transparent';
467 })
468 .on('click', function (d) {
469 if (_this.in_transition) {
470 return;
471 }
472 // Don't transition if there is no data to show
473 if (d.total === 0) {
474 return;
475 }
476 _this.in_transition = true;
477 // Set selected date to the one clicked on
478 _this.selected = d;
479 // Hide tooltip
480 _this.hideTooltip();
481 // Remove all year overview related items and labels
482 _this.removeYearOverview();
483 // Redraw the chart
484 _this.overview = 'day';
485 _this.drawChart();
486 })
487 .on('mouseover', function (d) {
488 if (_this.in_transition) {
489 return;
490 }
491 // Pulsating animation
492 var /** @type {?} */ circle = d3.select(d3.event.currentTarget);
493 var /** @type {?} */ repeat = function () {
494 circle.transition()
495 .duration(_this.transition_duration)
496 .ease(d3.easeLinear)
497 .attr('x', function (d) {
498 return _this.calcItemX(d, start_of_year) - (_this.item_size * 1.1 - _this.item_size) / 2;
499 })
500 .attr('y', function (d) {
501 return _this.calcItemY(d) - (_this.item_size * 1.1 - _this.item_size) / 2;
502 })
503 .attr('width', _this.item_size * 1.1)
504 .attr('height', _this.item_size * 1.1)
505 .transition()
506 .duration(_this.transition_duration)
507 .ease(d3.easeLinear)
508 .attr('x', function (d) {
509 return _this.calcItemX(d, start_of_year) + (_this.item_size - _this.calcItemSize(d, max_value)) / 2;
510 })
511 .attr('y', function (d) {
512 return _this.calcItemY(d) + (_this.item_size - _this.calcItemSize(d, max_value)) / 2;
513 })
514 .attr('width', function (d) {
515 return _this.calcItemSize(d, max_value);
516 })
517 .attr('height', function (d) {
518 return _this.calcItemSize(d, max_value);
519 })
520 .on('end', repeat);
521 };
522 repeat();
523 // Construct tooltip
524 var /** @type {?} */ tooltip_html = '';
525 tooltip_html += '<div class="header"><strong>' + (d.total ? _this.formatTime(d.total) : 'No time') + ' tracked</strong></div>';
526 tooltip_html += '<div>on ' + moment(d.date).format('dddd, MMM Do YYYY') + '</div><br>';
527 // Add summary to the tooltip
528 d.summary.map(function (d) {
529 tooltip_html += '<div><span><strong>' + d.name + '</strong></span>';
530 tooltip_html += '<span>' + _this.formatTime(d.value) + '</span></div>';
531 });
532 // Calculate tooltip position
533 var /** @type {?} */ x = _this.calcItemX(d, start_of_year) + _this.item_size;
534 if (_this.width - x < (_this.tooltip_width + _this.tooltip_padding * 3)) {
535 x -= _this.tooltip_width + _this.tooltip_padding * 2;
536 }
537 var /** @type {?} */ y = _this.calcItemY(d) + _this.item_size;
538 // Show tooltip
539 _this.tooltip.html(tooltip_html)
540 .style('left', x + 'px')
541 .style('top', y + 'px')
542 .transition()
543 .duration(_this.transition_duration / 2)
544 .ease(d3.easeLinear)
545 .style('opacity', 1);
546 })
547 .on('mouseout', function () {
548 if (_this.in_transition) {
549 return;
550 }
551 // Set circle radius back to what it's supposed to be
552 d3.select(d3.event.currentTarget).transition()
553 .duration(_this.transition_duration / 2)
554 .ease(d3.easeLinear)
555 .attr('x', function (d) {
556 return _this.calcItemX(d, start_of_year) + (_this.item_size - _this.calcItemSize(d, max_value)) / 2;
557 })
558 .attr('y', function (d) {
559 return _this.calcItemY(d) + (_this.item_size - _this.calcItemSize(d, max_value)) / 2;
560 })
561 .attr('width', function (d) {
562 return _this.calcItemSize(d, max_value);
563 })
564 .attr('height', function (d) {
565 return _this.calcItemSize(d, max_value);
566 });
567 // Hide tooltip
568 _this.hideTooltip();
569 })
570 .transition()
571 .delay(function () {
572 return (Math.cos(Math.PI * Math.random()) + 1) * _this.transition_duration;
573 })
574 .duration(function () {
575 return _this.transition_duration;
576 })
577 .ease(d3.easeLinear)
578 .style('opacity', 1)
579 .call(function (transition, callback) {
580 if (transition.empty()) {
581 callback();
582 }
583 var /** @type {?} */ n = 0;
584 transition
585 .each(function () { ++n; })
586 .on('end', function () {
587 if (!--n) {
588 callback.apply(this, arguments);
589 }
590 });
591 }, function () {
592 _this.in_transition = false;
593 });
594 // Add month labels
595 var /** @type {?} */ month_labels = d3.timeMonths(start_of_year.toDate(), end_of_year.toDate());
596 var /** @type {?} */ monthScale = d3.scaleLinear()
597 .range([0, this.width])
598 .domain([0, month_labels.length]);
599 this.labels.selectAll('.label-month').remove();
600 this.labels.selectAll('.label-month')
601 .data(month_labels)
602 .enter()
603 .append('text')
604 .attr('class', 'label label-month')
605 .attr('font-size', function () {
606 return Math.floor(_this.label_padding / 3) + 'px';
607 })
608 .text(function (d) {
609 return d.toLocaleDateString('en-us', { month: 'short' });
610 })
611 .attr('x', function (d, i) {
612 return monthScale(i) + (monthScale(i) - monthScale(i - 1)) / 2;
613 })
614 .attr('y', this.label_padding / 2)
615 .on('mouseenter', function (d) {
616 if (_this.in_transition) {
617 return;
618 }
619 var /** @type {?} */ selected_month = moment(d);
620 _this.items.selectAll('.item-circle')
621 .transition()
622 .duration(_this.transition_duration)
623 .ease(d3.easeLinear)
624 .style('opacity', function (d) {
625 return moment(d.date).isSame(selected_month, 'month') ? 1 : 0.1;
626 });
627 })
628 .on('mouseout', function () {
629 if (_this.in_transition) {
630 return;
631 }
632 _this.items.selectAll('.item-circle')
633 .transition()
634 .duration(_this.transition_duration)
635 .ease(d3.easeLinear)
636 .style('opacity', 1);
637 })
638 .on('click', function (d) {
639 if (_this.in_transition) {
640 return;
641 }
642 // Check month data
643 var /** @type {?} */ month_data = _this.data.filter(function (e) {
644 return moment(d).startOf('month') <= moment(e.date) && moment(e.date) < moment(d).endOf('month');
645 });
646 // Don't transition if there is no data to show
647 if (!month_data.length) {
648 return;
649 }
650 // Set selected month to the one clicked on
651 _this.selected = { date: d };
652 _this.in_transition = true;
653 // Hide tooltip
654 _this.hideTooltip();
655 // Remove all year overview related items and labels
656 _this.removeYearOverview();
657 // Redraw the chart
658 _this.overview = 'month';
659 _this.drawChart();
660 });
661 // Add day labels
662 var /** @type {?} */ day_labels = d3.timeDays(moment().startOf('week').toDate(), moment().endOf('week').toDate());
663 var /** @type {?} */ dayScale = d3.scaleBand()
664 .rangeRound([this.label_padding, this.height])
665 .domain(day_labels.map(function (d) {
666 return moment(d).weekday().toString();
667 }));
668 this.labels.selectAll('.label-day').remove();
669 this.labels.selectAll('.label-day')
670 .data(day_labels)
671 .enter()
672 .append('text')
673 .attr('class', 'label label-day')
674 .attr('x', this.label_padding / 3)
675 .attr('y', function (d, i) {
676 return dayScale((i).toString()) + dayScale.bandwidth() / 1.75;
677 })
678 .style('text-anchor', 'left')
679 .attr('font-size', function () {
680 return Math.floor(_this.label_padding / 3) + 'px';
681 })
682 .text(function (d) {
683 return moment(d).format('dddd')[0];
684 })
685 .on('mouseenter', function (d) {
686 if (_this.in_transition) {
687 return;
688 }
689 var /** @type {?} */ selected_day = moment(d);
690 _this.items.selectAll('.item-circle')
691 .transition()
692 .duration(_this.transition_duration)
693 .ease(d3.easeLinear)
694 .style('opacity', function (d) {
695 return (moment(d.date).day() === selected_day.day()) ? 1 : 0.1;
696 });
697 })
698 .on('mouseout', function () {
699 if (_this.in_transition) {
700 return;
701 }
702 _this.items.selectAll('.item-circle')
703 .transition()
704 .duration(_this.transition_duration)
705 .ease(d3.easeLinear)
706 .style('opacity', 1);
707 });
708 // Add button to switch back to previous overview
709 this.drawButton();
710 };
711 ;
712 /**
713 * Draw month overview
714 * @return {?}
715 */
716 CalendarHeatmap.prototype.drawMonthOverview = function () {
717 var _this = this;
718 // Add current overview to the history
719 if (this.history[this.history.length - 1] !== this.overview) {
720 this.history.push(this.overview);
721 }
722 // Define beginning and end of the month
723 var /** @type {?} */ start_of_month = moment(this.selected['date']).startOf('month');
724 var /** @type {?} */ end_of_month = moment(this.selected['date']).endOf('month');
725 // Filter data down to the selected month
726 var /** @type {?} */ month_data = this.data.filter(function (d) {
727 return start_of_month <= moment(d.date) && moment(d.date) < end_of_month;
728 });
729 var /** @type {?} */ max_value = d3.max(month_data, function (d) {
730 return d3.max(d.summary, function (d) {
731 return +d.value;
732 });
733 });
734 // Define day labels and axis
735 var /** @type {?} */ day_labels = d3.timeDays(moment().startOf('week').toDate(), moment().endOf('week').toDate());
736 var /** @type {?} */ dayScale = d3.scaleBand()
737 .rangeRound([this.label_padding, this.height])
738 .domain(day_labels.map(function (d) {
739 return moment(d).weekday().toString();
740 }));
741 // Define week labels and axis
742 var /** @type {?} */ week_labels = [start_of_month.clone()];
743 while (start_of_month.week() !== end_of_month.week()) {
744 week_labels.push(start_of_month.add(1, 'week').clone());
745 }
746 var /** @type {?} */ weekScale = d3.scaleBand()
747 .rangeRound([this.label_padding, this.width])
748 .padding(0.05)
749 .domain(week_labels.map(function (weekday) {
750 return weekday.week().toString();
751 }));
752 // Add month data items to the overview
753 this.items.selectAll('.item-block-month').remove();
754 var /** @type {?} */ item_block = this.items.selectAll('.item-block-month')
755 .data(month_data)
756 .enter()
757 .append('g')
758 .attr('class', 'item item-block-month')
759 .attr('width', function () {
760 return (_this.width - _this.label_padding) / week_labels.length - _this.gutter * 5;
761 })
762 .attr('height', function () {
763 return Math.min(dayScale.bandwidth(), _this.max_block_height);
764 })
765 .attr('transform', function (d) {
766 return 'translate(' + weekScale(moment(d.date).week().toString()) + ',' + ((dayScale(moment(d.date).weekday().toString()) + dayScale.bandwidth() / 1.75) - 15) + ')';
767 })
768 .attr('total', function (d) {
769 return d.total;
770 })
771 .attr('date', function (d) {
772 return d.date;
773 })
774 .attr('offset', 0)
775 .on('click', function (d) {
776 if (_this.in_transition) {
777 return;
778 }
779 // Don't transition if there is no data to show
780 if (d.total === 0) {
781 return;
782 }
783 _this.in_transition = true;
784 // Set selected date to the one clicked on
785 _this.selected = d;
786 // Hide tooltip
787 _this.hideTooltip();
788 // Remove all month overview related items and labels
789 _this.removeMonthOverview();
790 // Redraw the chart
791 _this.overview = 'day';
792 _this.drawChart();
793 });
794 var /** @type {?} */ item_width = (this.width - this.label_padding) / week_labels.length - this.gutter * 5;
795 var /** @type {?} */ itemScale = d3.scaleLinear()
796 .rangeRound([0, item_width]);
797 var /** @type {?} */ item_gutter = this.item_gutter;
798 item_block.selectAll('.item-block-rect')
799 .data(function (d) {
800 return d.summary;
801 })
802 .enter()
803 .append('rect')
804 .attr('class', 'item item-block-rect')
805 .attr('x', function (d) {
806 var /** @type {?} */ total = parseInt(d3.select(this.parentNode).attr('total'));
807 var /** @type {?} */ offset = parseInt(d3.select(this.parentNode).attr('offset'));
808 itemScale.domain([0, total]);
809 d3.select(this.parentNode).attr('offset', offset + itemScale(d.value));
810 return offset;
811 })
812 .attr('width', function (d) {
813 var /** @type {?} */ total = parseInt(d3.select(this.parentNode).attr('total'));
814 itemScale.domain([0, total]);
815 return Math.max((itemScale(d.value) - item_gutter), 1);
816 })
817 .attr('height', function () {
818 return Math.min(dayScale.bandwidth(), _this.max_block_height);
819 })
820 .attr('fill', function (d) {
821 var /** @type {?} */ color = d3.scaleLinear()
822 .range(['#ffffff', _this.color])
823 .domain([-0.15 * max_value, max_value]);
824 return color(d.value) || '#ff4500';
825 })
826 .style('opacity', 0)
827 .on('mouseover', function (d) {
828 if (_this.in_transition) {
829 return;
830 }
831 // Get date from the parent node
832 var /** @type {?} */ date = new Date(d3.select(d3.event.currentTarget.parentNode).attr('date'));
833 // Construct tooltip
834 var /** @type {?} */ tooltip_html = '';
835 tooltip_html += '<div class="header"><strong>' + d.name + '</strong></div><br>';
836 tooltip_html += '<div><strong>' + (d.value ? _this.formatTime(d.value) : 'No time') + ' tracked</strong></div>';
837 tooltip_html += '<div>on ' + moment(date).format('dddd, MMM Do YYYY') + '</div>';
838 // Calculate tooltip position
839 var /** @type {?} */ x = weekScale(moment(date).week().toString()) + _this.tooltip_padding;
840 while (_this.width - x < (_this.tooltip_width + _this.tooltip_padding * 3)) {
841 x -= 10;
842 }
843 var /** @type {?} */ y = dayScale(moment(date).weekday().toString()) + _this.tooltip_padding * 2;
844 // Show tooltip
845 _this.tooltip.html(tooltip_html)
846 .style('left', x + 'px')
847 .style('top', y + 'px')
848 .transition()
849 .duration(_this.transition_duration / 2)
850 .ease(d3.easeLinear)
851 .style('opacity', 1);
852 })
853 .on('mouseout', function () {
854 if (_this.in_transition) {
855 return;
856 }
857 _this.hideTooltip();
858 })
859 .transition()
860 .delay(function () {
861 return (Math.cos(Math.PI * Math.random()) + 1) * _this.transition_duration;
862 })
863 .duration(function () {
864 return _this.transition_duration;
865 })
866 .ease(d3.easeLinear)
867 .style('opacity', 1)
868 .call(function (transition, callback) {
869 if (transition.empty()) {
870 callback();
871 }
872 var /** @type {?} */ n = 0;
873 transition
874 .each(function () { ++n; })
875 .on('end', function () {
876 if (!--n) {
877 callback.apply(this, arguments);
878 }
879 });
880 }, function () {
881 _this.in_transition = false;
882 });
883 // Add week labels
884 this.labels.selectAll('.label-week').remove();
885 this.labels.selectAll('.label-week')
886 .data(week_labels)
887 .enter()
888 .append('text')
889 .attr('class', 'label label-week')
890 .attr('font-size', function () {
891 return Math.floor(_this.label_padding / 3) + 'px';
892 })
893 .text(function (d) {
894 return 'Week ' + d.week();
895 })
896 .attr('x', function (d) {
897 return weekScale(d.week());
898 })
899 .attr('y', this.label_padding / 2)
900 .on('mouseenter', function (weekday) {
901 if (_this.in_transition) {
902 return;
903 }
904 _this.items.selectAll('.item-block-month')
905 .transition()
906 .duration(_this.transition_duration)
907 .ease(d3.easeLinear)
908 .style('opacity', function (d) {
909 return (moment(d.date).week() === weekday.week()) ? 1 : 0.1;
910 });
911 })
912 .on('mouseout', function () {
913 if (_this.in_transition) {
914 return;
915 }
916 _this.items.selectAll('.item-block-month')
917 .transition()
918 .duration(_this.transition_duration)
919 .ease(d3.easeLinear)
920 .style('opacity', 1);
921 })
922 .on('click', function (d) {
923 if (_this.in_transition) {
924 return;
925 }
926 // Check week data
927 var /** @type {?} */ week_data = _this.data.filter(function (e) {
928 return d.startOf('week') <= moment(e.date) && moment(e.date) < d.endOf('week');
929 });
930 // Don't transition if there is no data to show
931 if (!week_data.length) {
932 return;
933 }
934 _this.in_transition = true;
935 // Set selected month to the one clicked on
936 _this.selected = { date: d };
937 // Hide tooltip
938 _this.hideTooltip();
939 // Remove all year overview related items and labels
940 _this.removeMonthOverview();
941 // Redraw the chart
942 _this.overview = 'week';
943 _this.drawChart();
944 });
945 // Add day labels
946 this.labels.selectAll('.label-day').remove();
947 this.labels.selectAll('.label-day')
948 .data(day_labels)
949 .enter()
950 .append('text')
951 .attr('class', 'label label-day')
952 .attr('x', this.label_padding / 3)
953 .attr('y', function (d, i) {
954 return dayScale(i) + dayScale.bandwidth() / 1.75;
955 })
956 .style('text-anchor', 'left')
957 .attr('font-size', function () {
958 return Math.floor(_this.label_padding / 3) + 'px';
959 })
960 .text(function (d) {
961 return moment(d).format('dddd')[0];
962 })
963 .on('mouseenter', function (d) {
964 if (_this.in_transition) {
965 return;
966 }
967 var /** @type {?} */ selected_day = moment(d);
968 _this.items.selectAll('.item-block-month')
969 .transition()
970 .duration(_this.transition_duration)
971 .ease(d3.easeLinear)
972 .style('opacity', function (d) {
973 return (moment(d.date).day() === selected_day.day()) ? 1 : 0.1;
974 });
975 })
976 .on('mouseout', function () {
977 if (_this.in_transition) {
978 return;
979 }
980 _this.items.selectAll('.item-block-month')
981 .transition()
982 .duration(_this.transition_duration)
983 .ease(d3.easeLinear)
984 .style('opacity', 1);
985 });
986 // Add button to switch back to previous overview
987 this.drawButton();
988 };
989 ;
990 /**
991 * Draw week overview
992 * @return {?}
993 */
994 CalendarHeatmap.prototype.drawWeekOverview = function () {
995 var _this = this;
996 // Add current overview to the history
997 if (this.history[this.history.length - 1] !== this.overview) {
998 this.history.push(this.overview);
999 }
1000 // Define beginning and end of the week
1001 var /** @type {?} */ start_of_week = moment(this.selected['date']).startOf('week');
1002 var /** @type {?} */ end_of_week = moment(this.selected['date']).endOf('week');
1003 // Filter data down to the selected week
1004 var /** @type {?} */ week_data = this.data.filter(function (d) {
1005 return start_of_week <= moment(d.date) && moment(d.date) < end_of_week;
1006 });
1007 var /** @type {?} */ max_value = d3.max(week_data, function (d) {
1008 return d3.max(d.summary, function (d) {
1009 return +d.value;
1010 });
1011 });
1012 // Define day labels and axis
1013 var /** @type {?} */ day_labels = d3.timeDays(moment().startOf('week').toDate(), moment().endOf('week').toDate());
1014 var /** @type {?} */ dayScale = d3.scaleBand()
1015 .rangeRound([this.label_padding, this.height])
1016 .domain(day_labels.map(function (d) {
1017 return moment(d).weekday().toString();
1018 }));
1019 // Define week labels and axis
1020 var /** @type {?} */ week_labels = [start_of_week];
1021 var /** @type {?} */ weekScale = d3.scaleBand()
1022 .rangeRound([this.label_padding, this.width])
1023 .padding(0.01)
1024 .domain(week_labels.map(function (weekday) {
1025 return weekday.week();
1026 }));
1027 // Add week data items to the overview
1028 this.items.selectAll('.item-block-week').remove();
1029 var /** @type {?} */ item_block = this.items.selectAll('.item-block-week')
1030 .data(week_data)
1031 .enter()
1032 .append('g')
1033 .attr('class', 'item item-block-week')
1034 .attr('width', function () {
1035 return (_this.width - _this.label_padding) / week_labels.length - _this.gutter * 5;
1036 })
1037 .attr('height', function () {
1038 return Math.min(dayScale.bandwidth(), _this.max_block_height);
1039 })
1040 .attr('transform', function (d) {
1041 return 'translate(' + weekScale(moment(d.date).week().toString()) + ',' + ((dayScale(moment(d.date).weekday().toString()) + dayScale.bandwidth() / 1.75) - 15) + ')';
1042 })
1043 .attr('total', function (d) {
1044 return d.total;
1045 })
1046 .attr('date', function (d) {
1047 return d.date;
1048 })
1049 .attr('offset', 0)
1050 .on('click', function (d) {
1051 if (_this.in_transition) {
1052 return;
1053 }
1054 // Don't transition if there is no data to show
1055 if (d.total === 0) {
1056 return;
1057 }
1058 _this.in_transition = true;
1059 // Set selected date to the one clicked on
1060 _this.selected = d;
1061 // Hide tooltip
1062 _this.hideTooltip();
1063 // Remove all week overview related items and labels
1064 _this.removeWeekOverview();
1065 // Redraw the chart
1066 _this.overview = 'day';
1067 _this.drawChart();
1068 });
1069 var /** @type {?} */ item_width = (this.width - this.label_padding) / week_labels.length - this.gutter * 5;
1070 var /** @type {?} */ itemScale = d3.scaleLinear()
1071 .rangeRound([0, item_width]);
1072 var /** @type {?} */ item_gutter = this.item_gutter;
1073 item_block.selectAll('.item-block-rect')
1074 .data(function (d) {
1075 return d.summary;
1076 })
1077 .enter()
1078 .append('rect')
1079 .attr('class', 'item item-block-rect')
1080 .attr('x', function (d) {
1081 var /** @type {?} */ total = parseInt(d3.select(this.parentNode).attr('total'));
1082 var /** @type {?} */ offset = parseInt(d3.select(this.parentNode).attr('offset'));
1083 itemScale.domain([0, total]);
1084 d3.select(this.parentNode).attr('offset', offset + itemScale(d.value));
1085 return offset;
1086 })
1087 .attr('width', function (d) {
1088 var /** @type {?} */ total = parseInt(d3.select(this.parentNode).attr('total'));
1089 itemScale.domain([0, total]);
1090 return Math.max((itemScale(d.value) - item_gutter), 1);
1091 })
1092 .attr('height', function () {
1093 return Math.min(dayScale.bandwidth(), _this.max_block_height);
1094 })
1095 .attr('fill', function (d) {
1096 var /** @type {?} */ color = d3.scaleLinear()
1097 .range(['#ffffff', _this.color])
1098 .domain([-0.15 * max_value, max_value]);
1099 return color(d.value) || '#ff4500';
1100 })
1101 .style('opacity', 0)
1102 .on('mouseover', function (d) {
1103 if (_this.in_transition) {
1104 return;
1105 }
1106 // Get date from the parent node
1107 var /** @type {?} */ date = new Date(d3.select(d3.event.currentTarget.parentNode).attr('date'));
1108 // Construct tooltip
1109 var /** @type {?} */ tooltip_html = '';
1110 tooltip_html += '<div class="header"><strong>' + d.name + '</strong></div><br>';
1111 tooltip_html += '<div><strong>' + (d.value ? _this.formatTime(d.value) : 'No time') + ' tracked</strong></div>';
1112 tooltip_html += '<div>on ' + moment(date).format('dddd, MMM Do YYYY') + '</div>';
1113 // Calculate tooltip position
1114 var /** @type {?} */ total = parseInt(d3.select(d3.event.currentTarget.parentNode).attr('total'));
1115 itemScale.domain([0, total]);
1116 var /** @type {?} */ x = parseInt(d3.select(d3.event.currentTarget).attr('x')) + itemScale(d.value) / 4 + _this.tooltip_width / 4;
1117 while (_this.width - x < (_this.tooltip_width + _this.tooltip_padding * 3)) {
1118 x -= 10;
1119 }
1120 var /** @type {?} */ y = dayScale(moment(date).weekday().toString()) + _this.tooltip_padding * 1.5;
1121 // Show tooltip
1122 _this.tooltip.html(tooltip_html)
1123 .style('left', x + 'px')
1124 .style('top', y + 'px')
1125 .transition()
1126 .duration(_this.transition_duration / 2)
1127 .ease(d3.easeLinear)
1128 .style('opacity', 1);
1129 })
1130 .on('mouseout', function () {
1131 if (_this.in_transition) {
1132 return;
1133 }
1134 _this.hideTooltip();
1135 })
1136 .transition()
1137 .delay(function () {
1138 return (Math.cos(Math.PI * Math.random()) + 1) * _this.transition_duration;
1139 })
1140 .duration(function () {
1141 return _this.transition_duration;
1142 })
1143 .ease(d3.easeLinear)
1144 .style('opacity', 1)
1145 .call(function (transition, callback) {
1146 if (transition.empty()) {
1147 callback();
1148 }
1149 var /** @type {?} */ n = 0;
1150 transition
1151 .each(function () { ++n; })
1152 .on('end', function () {
1153 if (!--n) {
1154 callback.apply(this, arguments);
1155 }
1156 });
1157 }, function () {
1158 _this.in_transition = false;
1159 });
1160 // Add week labels
1161 this.labels.selectAll('.label-week').remove();
1162 this.labels.selectAll('.label-week')
1163 .data(week_labels)
1164 .enter()
1165 .append('text')
1166 .attr('class', 'label label-week')
1167 .attr('font-size', function () {
1168 return Math.floor(_this.label_padding / 3) + 'px';
1169 })
1170 .text(function (d) {
1171 return 'Week ' + d.week();
1172 })
1173 .attr('x', function (d) {
1174 return weekScale(d.week());
1175 })
1176 .attr('y', this.label_padding / 2)
1177 .on('mouseenter', function (weekday) {
1178 if (_this.in_transition) {
1179 return;
1180 }
1181 _this.items.selectAll('.item-block-week')
1182 .transition()
1183 .duration(_this.transition_duration)
1184 .ease(d3.easeLinear)
1185 .style('opacity', function (d) {
1186 return (moment(d.date).week() === weekday.week()) ? 1 : 0.1;
1187 });
1188 })
1189 .on('mouseout', function () {
1190 if (_this.in_transition) {
1191 return;
1192 }
1193 _this.items.selectAll('.item-block-week')
1194 .transition()
1195 .duration(_this.transition_duration)
1196 .ease(d3.easeLinear)
1197 .style('opacity', 1);
1198 });
1199 // Add day labels
1200 this.labels.selectAll('.label-day').remove();
1201 this.labels.selectAll('.label-day')
1202 .data(day_labels)
1203 .enter()
1204 .append('text')
1205 .attr('class', 'label label-day')
1206 .attr('x', this.label_padding / 3)
1207 .attr('y', function (d, i) {
1208 return dayScale((i).toString()) + dayScale.bandwidth() / 1.75;
1209 })
1210 .style('text-anchor', 'left')
1211 .attr('font-size', function () {
1212 return Math.floor(_this.label_padding / 3) + 'px';
1213 })
1214 .text(function (d) {
1215 return moment(d).format('dddd')[0];
1216 })
1217 .on('mouseenter', function (d) {
1218 if (_this.in_transition) {
1219 return;
1220 }
1221 var /** @type {?} */ selected_day = moment(d);
1222 _this.items.selectAll('.item-block-week')
1223 .transition()
1224 .duration(_this.transition_duration)
1225 .ease(d3.easeLinear)
1226 .style('opacity', function (d) {
1227 return (moment(d.date).day() === selected_day.day()) ? 1 : 0.1;
1228 });
1229 })
1230 .on('mouseout', function () {
1231 if (_this.in_transition) {
1232 return;
1233 }
1234 _this.items.selectAll('.item-block-week')
1235 .transition()
1236 .duration(_this.transition_duration)
1237 .ease(d3.easeLinear)
1238 .style('opacity', 1);
1239 });
1240 // Add button to switch back to previous overview
1241 this.drawButton();
1242 };
1243 ;
1244 /**
1245 * Draw day overview
1246 * @return {?}
1247 */
1248 CalendarHeatmap.prototype.drawDayOverview = function () {
1249 var _this = this;
1250 // Add current overview to the history
1251 if (this.history[this.history.length - 1] !== this.overview) {
1252 this.history.push(this.overview);
1253 }
1254 // Initialize selected date to today if it was not set
1255 if (!Object.keys(this.selected).length) {
1256 this.selected = this.data[this.data.length - 1];
1257 }
1258 var /** @type {?} */ project_labels = this.selected['summary'].map(function (project) {
1259 return project.name;
1260 });
1261 var /** @type {?} */ projectScale = d3.scaleBand()
1262 .rangeRound([this.label_padding, this.height])
1263 .domain(project_labels);
1264 var /** @type {?} */ itemScale = d3.scaleTime()
1265 .range([this.label_padding * 2, this.width])
1266 .domain([moment(this.selected['date']).startOf('day'), moment(this.selected['date']).endOf('day')]);
1267 this.items.selectAll('.item-block').remove();
1268 this.items.selectAll('.item-block')
1269 .data(this.selected['details'])
1270 .enter()
1271 .append('rect')
1272 .attr('class', 'item item-block')
1273 .attr('x', function (d) {
1274 return itemScale(moment(d.date));
1275 })
1276 .attr('y', function (d) {
1277 return (projectScale(d.name) + projectScale.bandwidth() / 2) - 15;
1278 })
1279 .attr('width', function (d) {
1280 var /** @type {?} */ end = itemScale(d3.timeSecond.offset(moment(d.date).toDate(), d.value));
1281 return Math.max((end - itemScale(moment(d.date))), 1);
1282 })
1283 .attr('height', function () {
1284 return Math.min(projectScale.bandwidth(), _this.max_block_height);
1285 })
1286 .attr('fill', function () {
1287 return _this.color;
1288 })
1289 .style('opacity', 0)
1290 .on('mouseover', function (d) {
1291 if (_this.in_transition) {
1292 return;
1293 }
1294 // Construct tooltip
1295 var /** @type {?} */ tooltip_html = '';
1296 tooltip_html += '<div class="header"><strong>' + d.name + '</strong><div><br>';
1297 tooltip_html += '<div><strong>' + (d.value ? _this.formatTime(d.value) : 'No time') + ' tracked</strong></div>';
1298 tooltip_html += '<div>on ' + moment(d.date).format('dddd, MMM Do YYYY HH:mm') + '</div>';
1299 // Calculate tooltip position
1300 var /** @type {?} */ x = d.value * 100 / (60 * 60 * 24) + itemScale(moment(d.date));
1301 while (_this.width - x < (_this.tooltip_width + _this.tooltip_padding * 3)) {
1302 x -= 10;
1303 }
1304 var /** @type {?} */ y = projectScale(d.name) + projectScale.bandwidth() / 2 + _this.tooltip_padding / 2;
1305 // Show tooltip
1306 _this.tooltip.html(tooltip_html)
1307 .style('left', x + 'px')
1308 .style('top', y + 'px')
1309 .transition()
1310 .duration(_this.transition_duration / 2)
1311 .ease(d3.easeLinear)
1312 .style('opacity', 1);
1313 })
1314 .on('mouseout', function () {
1315 if (_this.in_transition) {
1316 return;
1317 }
1318 _this.hideTooltip();
1319 })
1320 .on('click', function (d) {
1321 if (_this.handler) {
1322 _this.handler.emit(d);
1323 }
1324 })
1325 .transition()
1326 .delay(function () {
1327 return (Math.cos(Math.PI * Math.random()) + 1) * _this.transition_duration;
1328 })
1329 .duration(function () {
1330 return _this.transition_duration;
1331 })
1332 .ease(d3.easeLinear)
1333 .style('opacity', 0.5)
1334 .call(function (transition, callback) {
1335 if (transition.empty()) {
1336 callback();
1337 }
1338 var /** @type {?} */ n = 0;
1339 transition
1340 .each(function () { ++n; })
1341 .on('end', function () {
1342 if (!--n) {
1343 callback.apply(this, arguments);
1344 }
1345 });
1346 }, function () {
1347 _this.in_transition = false;
1348 });
1349 // Add time labels
1350 var /** @type {?} */ timeLabels = d3.timeHours(moment(this.selected['date']).startOf('day').toDate(), moment(this.selected['date']).endOf('day').toDate());
1351 var /** @type {?} */ timeScale = d3.scaleTime()
1352 .range([this.label_padding * 2, this.width])
1353 .domain([0, timeLabels.length]);
1354 this.labels.selectAll('.label-time').remove();
1355 this.labels.selectAll('.label-time')
1356 .data(timeLabels)
1357 .enter()
1358 .append('text')
1359 .attr('class', 'label label-time')
1360 .attr('font-size', function () {
1361 return Math.floor(_this.label_padding / 3) + 'px';
1362 })
1363 .text(function (d) {
1364 return moment(d).format('HH:mm');
1365 })
1366 .attr('x', function (d, i) {
1367 return timeScale(i);
1368 })
1369 .attr('y', this.label_padding / 2)
1370 .on('mouseenter', function (d) {
1371 if (_this.in_transition) {
1372 return;
1373 }
1374 var /** @type {?} */ selected = itemScale(moment(d));
1375 _this.items.selectAll('.item-block')
1376 .transition()
1377 .duration(_this.transition_duration)
1378 .ease(d3.easeLinear)
1379 .style('opacity', function (d) {
1380 var /** @type {?} */ start = itemScale(moment(d.date));
1381 var /** @type {?} */ end = itemScale(moment(d.date).add(d.value, 'seconds'));
1382 return (selected >= start && selected <= end) ? 1 : 0.1;
1383 });
1384 })
1385 .on('mouseout', function () {
1386 if (_this.in_transition) {
1387 return;
1388 }
1389 _this.items.selectAll('.item-block')
1390 .transition()
1391 .duration(_this.transition_duration)
1392 .ease(d3.easeLinear)
1393 .style('opacity', 0.5);
1394 });
1395 // Add project labels
1396 var /** @type {?} */ label_padding = this.label_padding;
1397 this.labels.selectAll('.label-project').remove();
1398 this.labels.selectAll('.label-project')
1399 .data(project_labels)
1400 .enter()
1401 .append('text')
1402 .attr('class', 'label label-project')
1403 .attr('x', this.gutter)
1404 .attr('y', function (d) {
1405 return projectScale(d) + projectScale.bandwidth() / 2;
1406 })
1407 .attr('min-height', function () {
1408 return projectScale.bandwidth();
1409 })
1410 .style('text-anchor', 'left')
1411 .attr('font-size', function () {
1412 return Math.floor(_this.label_padding / 3) + 'px';
1413 })
1414 .text(function (d) {
1415 return d;
1416 })
1417 .each(function (d, i) {
1418 var /** @type {?} */ obj = d3.select(this), /** @type {?} */ text_length = obj.node().getComputedTextLength(), /** @type {?} */ text = obj.text();
1419 while (text_length > (label_padding * 1.5) && text.length > 0) {
1420 text = text.slice(0, -1);
1421 obj.text(text + '...');
1422 text_length = obj.node().getComputedTextLength();
1423 }
1424 })
1425 .on('mouseenter', function (project) {
1426 if (_this.in_transition) {
1427 return;
1428 }
1429 _this.items.selectAll('.item-block')
1430 .transition()
1431 .duration(_this.transition_duration)
1432 .ease(d3.easeLinear)
1433 .style('opacity', function (d) {
1434 return (d.name === project) ? 1 : 0.1;
1435 });
1436 })
1437 .on('mouseout', function () {
1438 if (_this.in_transition) {
1439 return;
1440 }
1441 _this.items.selectAll('.item-block')
1442 .transition()
1443 .duration(_this.transition_duration)
1444 .ease(d3.easeLinear)
1445 .style('opacity', 0.5);
1446 });
1447 // Add button to switch back to previous overview
1448 this.drawButton();
1449 };
1450 ;
1451 /**
1452 * Helper function to calculate item position on the x-axis
1453 * @param {?} d object
1454 * @param {?} start_of_year
1455 * @return {?}
1456 */
1457 CalendarHeatmap.prototype.calcItemX = function (d, start_of_year) {
1458 var /** @type {?} */ date = moment(d.date);
1459 var /** @type {?} */ dayIndex = Math.round((+date - +moment(start_of_year).startOf('week')) / 86400000);
1460 var /** @type {?} */ colIndex = Math.trunc(dayIndex / 7);
1461 return colIndex * (this.item_size + this.gutter) + this.label_padding;
1462 };
1463 ;
1464 /**
1465 * Helper function to calculate item position on the y-axis
1466 * @param {?} d object
1467 * @return {?}
1468 */
1469 CalendarHeatmap.prototype.calcItemY = function (d) {
1470 return this.label_padding + moment(d.date).weekday() * (this.item_size + this.gutter);
1471 };
1472 ;
1473 /**
1474 * Helper function to calculate item size
1475 * @param {?} d object
1476 * @param {?} max number
1477 * @return {?}
1478 */
1479 CalendarHeatmap.prototype.calcItemSize = function (d, max) {
1480 if (max <= 0) {
1481 return this.item_size;
1482 }
1483 return this.item_size * 0.75 + (this.item_size * d.total / max) * 0.25;
1484 };
1485 ;
1486 /**
1487 * Draw the button for navigation purposes
1488 * @return {?}
1489 */
1490 CalendarHeatmap.prototype.drawButton = function () {
1491 var _this = this;
1492 this.buttons.selectAll('.button').remove();
1493 var /** @type {?} */ button = this.buttons.append('g')
1494 .attr('class', 'button button-back')
1495 .style('opacity', 0)
1496 .on('click', function () {
1497 if (_this.in_transition) {
1498 return;
1499 }
1500 // Set transition boolean
1501 _this.in_transition = true;
1502 // Clean the canvas from whichever overview type was on
1503 if (_this.overview === 'year') {
1504 _this.removeYearOverview();
1505 }
1506 else if (_this.overview === 'month') {
1507 _this.removeMonthOverview();
1508 }
1509 else if (_this.overview === 'week') {
1510 _this.removeWeekOverview();
1511 }
1512 else if (_this.overview === 'day') {
1513 _this.removeDayOverview();
1514 }
1515 // Redraw the chart
1516 _this.history.pop();
1517 _this.overview = _this.history.pop();
1518 _this.drawChart();
1519 });
1520 button.append('circle')
1521 .attr('cx', this.label_padding / 2.25)
1522 .attr('cy', this.label_padding / 2.5)
1523 .attr('r', this.item_size / 2);
1524 button.append('text')
1525 .attr('x', this.label_padding / 2.25)
1526 .attr('y', this.label_padding / 2.5)
1527 .attr('dy', function () {
1528 return Math.floor(_this.width / 100) / 3;
1529 })
1530 .attr('font-size', function () {
1531 return Math.floor(_this.label_padding / 3) + 'px';
1532 })
1533 .html('&#x2190;');
1534 button.transition()
1535 .duration(this.transition_duration)
1536 .ease(d3.easeLinear)
1537 .style('opacity', 1);
1538 };
1539 ;
1540 /**
1541 * Transition and remove items and labels related to global overview
1542 * @return {?}
1543 */
1544 CalendarHeatmap.prototype.removeGlobalOverview = function () {
1545 this.items.selectAll('.item-block-year')
1546 .transition()
1547 .duration(this.transition_duration)
1548 .ease(d3.easeLinear)
1549 .style('opacity', 0)
1550 .remove();
1551 this.labels.selectAll('.label-year').remove();
1552 };
1553 ;
1554 /**
1555 * Transition and remove items and labels related to year overview
1556 * @return {?}
1557 */
1558 CalendarHeatmap.prototype.removeYearOverview = function () {
1559 this.items.selectAll('.item-circle')
1560 .transition()
1561 .duration(this.transition_duration)
1562 .ease(d3.easeLinear)
1563 .style('opacity', 0)
1564 .remove();
1565 this.labels.selectAll('.label-day').remove();
1566 this.labels.selectAll('.label-month').remove();
1567 this.hideBackButton();
1568 };
1569 ;
1570 /**
1571 * Transition and remove items and labels related to month overview
1572 * @return {?}
1573 */
1574 CalendarHeatmap.prototype.removeMonthOverview = function () {
1575 var _this = this;
1576 this.items.selectAll('.item-block-month').selectAll('.item-block-rect')
1577 .transition()
1578 .duration(this.transition_duration)
1579 .ease(d3.easeLinear)
1580 .style('opacity', 0)
1581 .attr('x', function (d, i) {
1582 return (i % 2 === 0) ? -_this.width / 3 : _this.width / 3;
1583 })
1584 .remove();
1585 this.labels.selectAll('.label-day').remove();
1586 this.labels.selectAll('.label-week').remove();
1587 this.hideBackButton();
1588 };
1589 ;
1590 /**
1591 * Transition and remove items and labels related to week overview
1592 * @return {?}
1593 */
1594 CalendarHeatmap.prototype.removeWeekOverview = function () {
1595 var _this = this;
1596 this.items.selectAll('.item-block-week').selectAll('.item-block-rect')
1597 .transition()
1598 .duration(this.transition_duration)
1599 .ease(d3.easeLinear)
1600 .style('opacity', 0)
1601 .attr('x', function (d, i) {
1602 return (i % 2 === 0) ? -_this.width / 3 : _this.width / 3;
1603 })
1604 .remove();
1605 this.labels.selectAll('.label-day').remove();
1606 this.labels.selectAll('.label-week').remove();
1607 this.hideBackButton();
1608 };
1609 ;
1610 /**
1611 * Transition and remove items and labels related to daily overview
1612 * @return {?}
1613 */
1614 CalendarHeatmap.prototype.removeDayOverview = function () {
1615 var _this = this;
1616 this.items.selectAll('.item-block')
1617 .transition()
1618 .duration(this.transition_duration)
1619 .ease(d3.easeLinear)
1620 .style('opacity', 0)
1621 .attr('x', function (d, i) {
1622 return (i % 2 === 0) ? -_this.width / 3 : _this.width / 3;
1623 })
1624 .remove();
1625 this.labels.selectAll('.label-time').remove();
1626 this.labels.selectAll('.label-project').remove();
1627 this.hideBackButton();
1628 };
1629 ;
1630 /**
1631 * Helper function to hide the tooltip
1632 * @return {?}
1633 */
1634 CalendarHeatmap.prototype.hideTooltip = function () {
1635 this.tooltip.transition()
1636 .duration(this.transition_duration / 2)
1637 .ease(d3.easeLinear)
1638 .style('opacity', 0);
1639 };
1640 ;
1641 /**
1642 * Helper function to hide the back button
1643 * @return {?}
1644 */
1645 CalendarHeatmap.prototype.hideBackButton = function () {
1646 this.buttons.selectAll('.button')
1647 .transition()
1648 .duration(this.transition_duration)
1649 .ease(d3.easeLinear)
1650 .style('opacity', 0)
1651 .remove();
1652 };
1653 ;
1654 /**
1655 * Helper function to convert seconds to a human readable format
1656 * @param {?} seconds Integer
1657 * @return {?}
1658 */
1659 CalendarHeatmap.prototype.formatTime = function (seconds) {
1660 var /** @type {?} */ hours = Math.floor(seconds / 3600);
1661 var /** @type {?} */ minutes = Math.floor((seconds - (hours * 3600)) / 60);
1662 var /** @type {?} */ time = '';
1663 if (hours > 0) {
1664 time += hours === 1 ? '1 hour ' : hours + ' hours ';
1665 }
1666 if (minutes > 0) {
1667 time += minutes === 1 ? '1 minute' : minutes + ' minutes';
1668 }
1669 if (hours === 0 && minutes === 0) {
1670 time = Math.round(seconds) + ' seconds';
1671 }
1672 return time;
1673 };
1674 ;
1675 CalendarHeatmap.decorators = [
1676 { type: Component, args: [{
1677 selector: 'calendar-heatmap',
1678 template: "<div #root></div>",
1679 styles: ["\n :host {\n user-select: none;\n -ms-user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n }\n :host >>> .item {\n cursor: pointer;\n }\n :host >>> .label {\n cursor: pointer;\n fill: rgb(170, 170, 170);\n font-family: Helvetica, arial, 'Open Sans', sans-serif;\n }\n :host >>> .button {\n cursor: pointer;\n fill: transparent;\n stroke-width: 2;\n stroke: rgb(170, 170, 170);\n }\n :host >>> .button text {\n stroke-width: 1;\n text-anchor: middle;\n fill: rgb(170, 170, 170);\n }\n :host >>> .heatmap-tooltip {\n pointer-events: none;\n position: absolute;\n z-index: 9999;\n width: 250px;\n max-width: 250px;\n overflow: hidden;\n padding: 15px;\n font-size: 12px;\n line-height: 14px;\n color: rgb(51, 51, 51);\n font-family: Helvetica, arial, 'Open Sans', sans-serif;\n background: rgba(255, 255, 255, 0.75);\n }\n :host >>> .heatmap-tooltip .header strong {\n display: inline-block;\n width: 250px;\n }\n :host >>> .heatmap-tooltip span {\n display: inline-block;\n width: 50%;\n padding-right: 10px;\n box-sizing: border-box;\n }\n :host >>> .heatmap-tooltip span,\n :host >>> .heatmap-tooltip .header strong {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n "],
1680 },] },
1681 ];
1682 /**
1683 * @nocollapse
1684 */
1685 CalendarHeatmap.ctorParameters = function () { return []; };
1686 CalendarHeatmap.propDecorators = {
1687 'element': [{ type: ViewChild, args: ['root',] },],
1688 'data': [{ type: Input },],
1689 'color': [{ type: Input },],
1690 'overview': [{ type: Input },],
1691 'handler': [{ type: Output },],
1692 'onResize': [{ type: HostListener, args: ['window:resize', ['$event'],] },],
1693 };
1694 return CalendarHeatmap;
1695}());
1696export { CalendarHeatmap };
1697function CalendarHeatmap_tsickle_Closure_declarations() {
1698 /** @type {?} */
1699 CalendarHeatmap.decorators;
1700 /**
1701 * @nocollapse
1702 * @type {?}
1703 */
1704 CalendarHeatmap.ctorParameters;
1705 /** @type {?} */
1706 CalendarHeatmap.propDecorators;
1707 /** @type {?} */
1708 CalendarHeatmap.prototype.element;
1709 /** @type {?} */
1710 CalendarHeatmap.prototype.data;
1711 /** @type {?} */
1712 CalendarHeatmap.prototype.color;
1713 /** @type {?} */
1714 CalendarHeatmap.prototype.overview;
1715 /** @type {?} */
1716 CalendarHeatmap.prototype.handler;
1717 /** @type {?} */
1718 CalendarHeatmap.prototype.gutter;
1719 /** @type {?} */
1720 CalendarHeatmap.prototype.item_gutter;
1721 /** @type {?} */
1722 CalendarHeatmap.prototype.width;
1723 /** @type {?} */
1724 CalendarHeatmap.prototype.height;
1725 /** @type {?} */
1726 CalendarHeatmap.prototype.item_size;
1727 /** @type {?} */
1728 CalendarHeatmap.prototype.label_padding;
1729 /** @type {?} */
1730 CalendarHeatmap.prototype.max_block_height;
1731 /** @type {?} */
1732 CalendarHeatmap.prototype.transition_duration;
1733 /** @type {?} */
1734 CalendarHeatmap.prototype.in_transition;
1735 /** @type {?} */
1736 CalendarHeatmap.prototype.tooltip_width;
1737 /** @type {?} */
1738 CalendarHeatmap.prototype.tooltip_padding;
1739 /** @type {?} */
1740 CalendarHeatmap.prototype.history;
1741 /** @type {?} */
1742 CalendarHeatmap.prototype.selected;
1743 /** @type {?} */
1744 CalendarHeatmap.prototype.svg;
1745 /** @type {?} */
1746 CalendarHeatmap.prototype.items;
1747 /** @type {?} */
1748 CalendarHeatmap.prototype.labels;
1749 /** @type {?} */
1750 CalendarHeatmap.prototype.buttons;
1751 /** @type {?} */
1752 CalendarHeatmap.prototype.tooltip;
1753}
1754//# sourceMappingURL=calendar-heatmap.component.js.map
\No newline at end of file